C # 또는 Java와 같은 언어로 대수 데이터 형식을 어떻게 인코딩합니까?


58

대수 데이터 형식으로 쉽게 해결할 수있는 몇 가지 문제가 있습니다. 예를 들어 목록 형식은 다음과 같이 간결하게 표현할 수 있습니다.

data ConsList a = Empty | ConsCell a (ConsList a)

consmap f Empty          = Empty
consmap f (ConsCell a b) = ConsCell (f a) (consmap f b)

l = ConsCell 1 (ConsCell 2 (ConsCell 3 Empty))
consmap (+1) l

이 특정 예제는 Haskell에 있지만 대수 데이터 형식을 기본적으로 지원하는 다른 언어에서도 비슷합니다.

OO 스타일 하위 유형에 대한 명확한 매핑이 있음이 밝혀졌습니다. 데이터 유형은 추상 기본 클래스가되고 모든 데이터 생성자는 구체적인 하위 클래스가됩니다. 스칼라의 예는 다음과 같습니다.

sealed abstract class ConsList[+T] {
  def map[U](f: T => U): ConsList[U]
}

object Empty extends ConsList[Nothing] {
  override def map[U](f: Nothing => U) = this
}

final class ConsCell[T](first: T, rest: ConsList[T]) extends ConsList[T] {
  override def map[U](f: T => U) = new ConsCell(f(first), rest.map(f))
}

val l = (new ConsCell(1, new ConsCell(2, new ConsCell(3, Empty)))
l.map(1+)

순진한 서브 클래 싱 외에 필요한 것은 클래스를 봉인 하는 방법, 즉 서브 클래스를 계층 구조에 추가하는 것을 불가능하게하는 방법입니다.

C # 또는 Java와 같은 언어로이 문제에 어떻게 접근 하시겠습니까? C #에서 대수 데이터 형식을 사용하려고 할 때 발견 한 두 가지 걸림돌은 다음과 같습니다.

  • 나는 C #에서 하단 유형이 무엇인지 알 수 없었습니다 (즉, 무엇을 넣을 지 알 수 없었습니다 class Empty : ConsList< ??? >)
  • 하위 클래스를 계층 구조에 추가 할 수 없도록 봉인 하는 방법을 알 수 없었습니다.ConsList

C # 및 / 또는 Java에서 대수 데이터 형식을 구현하는 가장 관용적 인 방법은 무엇입니까? 또는 가능하지 않은 경우 관용적 대체물은 무엇입니까?



3
C #은 OOP 언어입니다. OOP를 사용하여 문제를 해결하십시오. 다른 패러다임을 사용하지 마십시오.
Euphoric

7
@Euphoric C #은 C # 3.0에서 매우 유용한 기능 언어가되었습니다. 일류 기능, 내장 공통 기능 작업, 모나드.
Mauricio Scheffer 1

2
@Euphoric : 일부 도메인은 객체로 모델링하기 쉽고 대수 데이터 유형으로 모델링하기가 어렵습니다. 두 가지를 모두 수행하는 방법을 알면 도메인 모델링에 더 많은 유연성이 제공됩니다. 그리고 내가 말했듯이, 대수적 데이터 유형을 전형적인 OO 개념에 매핑하는 것은 그렇게 복잡하지 않습니다. 데이터 유형이 추상 기본 클래스 (또는 인터페이스 또는 추상 특성)가되고 데이터 생성자는 구체적인 구현 서브 클래스가됩니다. 그것은 당신에게 열린 대수 데이터 타입을 제공합니다. 상속에 대한 제한은 닫힌 대수 데이터 형식을 제공합니다. 다형성은 대소 문자를 구별합니다.
Jörg W Mittag

3
@Euphoric, 패러다임, schmaradigm, 누가 신경 쓰나요? ADT는 기능적 프로그래밍 (또는 OOP 등)과 직교합니다. 모든 언어의 AST 인코딩은 적절한 ADT 지원 없이는 상당히 고통스럽고 언어를 컴파일하는 또 다른 패러다임 불가 지 기능인 패턴 일치가 없으면 고통입니다.
SK-logic

답변:


42

Java에서 클래스를 봉인하는 쉬운 방법이 있지만 보일러 플레이트가 있습니다. 개인 생성자를 기본 클래스에 넣은 다음 하위 클래스를 내부 클래스로 만듭니다.

public abstract class List<A> {

   // private constructor is uncallable by any sublclasses except inner classes
   private List() {
   }

   public static final class Nil<A> extends List<A> {
   }

   public static final class Cons<A> extends List<A> {
      public final A head;
      public final List<A> tail;

      public Cons(A head, List<A> tail) {
         this.head = head;
         this.tail = tail;
      }
   }
}

파견을 위해 방문자 패턴을 확인하십시오.

내 프로젝트 jADT : Java Algebraic DataTypes가 보일러 플레이트를 모두 생성합니다 https://github.com/JamesIry/jADT


2
어쨌든 나는 당신의 이름이 여기에 나타나는 것을보고 놀랍지 않습니다! 고마워, 나는이 관용구를 몰랐다.
Jörg W Mittag

4
당신이 "boilerplate heavy"라고 말했을 때 나는 훨씬 더 나쁜 것을 준비했다 ;-) Java는 상용구에 대해 때때로 나쁘다.
Joachim Sauer

그러나 이것은 구성하지 않습니다 : 당신은 캐스트를 통해 그것을 주장하지 않고 타입 A를 전문화 할 수있는 방법이 없습니다 (생각합니다)
nicolas

불행히도 더 복잡한 합계 유형을 나타낼 수없는 것 같습니다 (예 :) Either. 내 질문
Zoey Hewll

20

방문자 패턴 을 사용하면 패턴 일치를 보완 할 수 있습니다 . 예를 들어

data List a = Nil | Cons { value :: a, sublist :: List a }

Java로 작성할 수 있습니다

interface List<T> {
    public <R> R accept(Visitor<T,R> visitor);

    public static interface Visitor<T,R> {
        public R visitNil();
        public R visitCons(T value, List<T> sublist);
    }
}

final class Nil<T> implements List<T> {
    public Nil() { }

    public <R> R accept(Visitor<T,R> visitor) {
        return visitor.visitNil();
    }
}
final class Cons<T> implements List<T> {
    public final T value;
    public final List<T> sublist;

    public Cons(T value, List<T> sublist) {
        this.value = value;
        this.sublist = sublist;
    }

    public <R> R accept(Visitor<T,R> visitor) {
        return visitor.visitCons(value, sublist);
    }
}

실링은 Visitor클래스에 의해 달성됩니다 . 각 메소드는 서브 클래스 중 하나를 해체하는 방법을 선언합니다. 더 많은 서브 클래스를 추가 할 수 있지만 메소드 accept중 하나를 호출 하여 구현해야 visit...하므로 like Cons또는 like 동작해야합니다 Nil.


13

C # 명명 된 매개 변수 (C # 4.0에 도입 됨)를 남용하는 경우 다음과 같이 쉽게 대수 데이터 형식을 만들 수 있습니다.

Either<string, string> e = MonthName(2);

// Match with no return value.
e.Match
(
    Left: err => { Console.WriteLine("Could not convert month: {0}", err); },
    Right: name => { Console.WriteLine("The month is {0}", name); }
);

// Match with a return value.
string monthName =
    e.Match
    (
        Left: err => null,
        Right: name => name
    );
Console.WriteLine("monthName: {0}", monthName);

Either클래스 구현은 다음과 같습니다 .

public abstract class Either<L, R>
{
    // Subclass implementation calls the appropriate continuation.
    public abstract T Match<T>(Func<L, T> Left, Func<R, T> Right);

    // Convenience wrapper for when the caller doesn't want to return a value
    // from the match expression.
    public void Match(Action<L> Left, Action<R> Right)
    {
        this.Match<int>(
            Left: x => { Left(x); return 0; },
            Right: x => { Right(x); return 0; }
        );
    }
}

public class Left<L, R> : Either<L, R>
{
    L Value {get; set;}

    public Left(L Value)
    {
        this.Value = Value;
    }

    public override T Match<T>(Func<L, T> Left, Func<R, T> Right)
    {
        return Left(Value);
    }
}

public class Right<L, R> : Either<L, R>
{
    R Value { get; set; }

    public Right(R Value)
    {
        this.Value = Value;
    }

    public override T Match<T>(Func<L, T> Left, Func<R, T> Right)
    {
        return Right(Value);
    }
}

이전에는이 ​​기술의 Java 버전을 보았지만 람다 및 명명 된 매개 변수는이 기술을 매우 읽기 쉽게 만듭니다. +1!
Doval

1
여기서 문제는 Right가 오류 유형에 대해 일반적이지 않다는 것입니다. 같은 뭔가 : class Right<R> : Either<Bot,R>하나는 공변 (아웃)로 인터페이스로 변경, 매개 변수를 입력하고 봇 (객체의 맞은 편에, 다른 모든 유형의 하위 유형) 바닥 유형입니다. C #에 하단 유형이 있다고 생각하지 않습니다.
croyd 2019

5

C #에서는 수정 Empty으로 인해 기본 유형이 멤버 유형마다 다르기 때문에 해당 유형을 가질 수 없습니다 . 당신은 오직 가질 수 있습니다 Empty<T>; 그다지 유용하지 않습니다.

Java에서는 Empty : ConsList유형 삭제로 인해 발생할 수 있지만 유형 검사기가 어딘가에서 비명을 지르지 않을지 확실하지 않습니다.

그러나 두 언어 모두가 있으므로 모든 참조 유형을 "무엇이든" null이라고 생각할 수 있습니다 . 따라서 파생 항목을 지정하지 않으려면 "빈"으로 만 사용하면 됩니다.null


문제 null는 그것이 너무 일반적이라는 것입니다 : 그것은 아무것도 없다는 , 즉 일반적으로 공허함 을 나타냅니다. 그러나 목록 요소가 없음을 나타냅니다. 빈 목록과 빈 트리에는 고유 한 유형이 있어야합니다. 또한 빈 목록은 여전히 ​​고유 한 동작을 가지므로 실제 값이어야하므로 고유 한 방법이 필요합니다. 목록을 구성하기 위해 [1, 2, 3], 내가하고 싶은 말 Empty.prepend(3).prepend(2).prepend(1)(또는 오른쪽 연관 사업자와의 언어 1 :: 2 :: 3 :: Empty),하지만 난 말할 수 없다 null.prepend ….
Jörg W Mittag

@ JörgWMittag : 널에는 고유 한 유형이 있습니다. 목적에 따라 null 값을 사용하여 유형이 지정된 상수를 쉽게 만들 수도 있습니다. 그러나 메소드를 호출 할 수 없다는 것은 사실입니다. 어쨌든 요소 유형별 빈이 없으면 메소드에 대한 접근 방식이 작동하지 않습니다.
Jan Hudec

일부 교묘 한 확장 메서드는 null에 대한 '방법'호출을 가짜로 만들 수 있습니다 (물론 모두 정적으로 정적입니다)
jk.

당신은 할 수 Empty와를 Empty<>원하는 경우, 상당히 실제적인 시뮬레이션을 할 수 있도록 암시 적 변환 연산자를 남용. 기본적 Empty으로 코드에서 사용하지만 모든 유형 서명 등은 일반 변형 만 사용합니다.
Eamon Nerbonne

3

순진한 서브 클래 싱 외에 필요한 것은 클래스를 봉인하는 방법, 즉 서브 클래스를 계층 구조에 추가하는 것을 불가능하게하는 방법입니다.

자바에서는 할 수 없습니다. 그러나 기본 클래스를 개인 패키지로 선언 할 수 있습니다. 즉, 모든 직접 서브 클래스는 기본 클래스와 동일한 패키지에 속해야합니다. 그런 다음 서브 클래스를 final로 선언하면 더 이상 서브 클래 싱 할 수 없습니다.

그래도 이것이 실제 문제를 해결할 수 있을지 모르겠습니다 ...


나는 실제 문제가 없거나, 여기가 아니라 StackOverflow에 이것을 게시했을 것입니다 :-) 대수 데이터 형식의 중요한 속성은 닫을 수 있다는 것 입니다.이 경우 사례 수가 수정됩니다. 목록이 비어 있거나 비어 있지 않습니다. 이 경우를 정적으로 확인할 수 있으면 동적 캐스트 또는 동적 intanceof검사를 "의사 유형 안전"(예 : 컴파일러가 안전하지 않더라도 안전하다는 것을 알 수 있음)을 항상 확인할 수 있습니다. 그 두 가지 경우를 확인하십시오. 그러나 누군가 다른 하위 클래스를 추가하면 예상치 못한 런타임 오류가 발생할 수 있습니다.
Jörg W Mittag

@ JörgWMittag-Java는 분명히 원하는 것을 강력하게 지원하지 않습니다. 물론 런타임에 원하지 않는 하위 입력을 차단하기 위해 다양한 작업을 수행 할 수 있지만 "예상치 않은 런타임 오류"가 발생합니다.
Stephen C

3

데이터 유형 ConsList<A>은 인터페이스로 표시 될 수 있습니다. 인터페이스는 단일 유형을 노출시켜 deconstruct해당 유형의 값을 "해체"할 수 있습니다. 즉, 가능한 각 생성자를 처리합니다. deconstruct메소드 호출 case of은 Haskell 또는 ML 의 양식 과 유사합니다 .

interface ConsList<A> {
  <R> R deconstruct(
    Function<Unit, R> emptyCase,
    Function<Pair<A,ConsList<A>>, R> consCase
  );
}

deconstruct메소드는 ADT의 각 생성자에 대해 "콜백"함수를 사용합니다. 우리의 경우, 빈리스트 케이스를위한 함수와 "cons cell"케이스를위한 다른 함수를 취합니다.

각 콜백 함수는 생성자가 허용하는 값을 인수로 허용합니다. 따라서 "빈 목록"사례에는 인수가 없지만 "cons 셀"사례에는 목록의 머리와 꼬리라는 두 가지 인수가 있습니다.

Tuple클래스 또는 카레를 사용하여 이러한 "다중 인수"를 인코딩 할 수 있습니다 . 이 예제에서는 간단한 Pair클래스 를 사용하기로했습니다 .

인터페이스는 각 생성자마다 한 번씩 구현됩니다. 먼저 "빈 목록"을 구현했습니다. deconstruct구현은 단순히 호출하는 emptyCase콜백 함수를.

class ConsListEmpty<A> implements ConsList<A> {
  public ConsListEmpty() {}

  public <R> R deconstruct(
    Function<Unit, R> emptyCase,
    Function<Pair<A,ConsList<A>>, R> consCase
  ) {
    return emptyCase.apply(new Unit());
  }
}

그런 다음 "cons cell"사례를 비슷하게 구현합니다. 이번에는 클래스에 비어 있지 않은 목록의 머리와 꼬리가 있습니다. 에서 deconstruct구현, 이러한 속성은에 전달되는 consCase콜백 함수.

class ConsListConsCell<A> implements ConsList<A> {
  private A head;
  private ConsList<A> tail;

  public ConsListCons(A head, ConsList<A> tail) {
    this.head = head;
    this.tail = tail;
  }

  public <R> R deconstruct(
    Function<Unit, R> emptyCase,
    Function<Pair<A,ConsList<A>>, R> consCase
  ) {
    return consCase.apply(new Pair<A,ConsList<A>>(this.head, this.tail));
  }
}

이 ADT 인코딩을 사용하는 예는 다음과 같습니다 reduce. 일반적인 fold over 목록 인 함수를 작성할 수 있습니다 .

<T> T reduce(Function<Pair<T,A>,T> reducer, T initial, ConsList<T> l) {
  return l.deconstruct(
    ((unit) -> initial),
    ((t) -> reduce(reducer, reducer.apply(initial, t.v1), t.v2))
  );
}

이것은 Haskell의이 구현과 유사합니다.

reduce reducer initial l = case l of
  Empty -> initial
  Cons t_v1 t_v2  -> reduce reducer (reducer initial t_v1) t_v2

재미있는 접근 방식, 아주 좋은! F # Active Patterns 및 Scala Extractors에 대한 연결을 볼 수 있습니다 (아쉽게도 아무것도 모르는 Haskell Views에 대한 링크가있을 수 있습니다). 데이터 생성자에 대한 패턴 일치에 대한 책임을 ADT 인스턴스 자체로 옮길 생각은 없었습니다.
Jörg W Mittag

2

순진한 서브 클래 싱 외에 필요한 것은 클래스를 봉인하는 방법, 즉 서브 클래스를 계층 구조에 추가하는 것을 불가능하게하는 방법입니다.

C # 또는 Java와 같은 언어로이 문제에 어떻게 접근 하시겠습니까?

이를 수행하는 좋은 방법은 없지만, 끔찍한 해킹으로 살려는 경우 추상 기본 클래스의 생성자에 명시 적 유형 검사를 추가 할 수 있습니다. Java에서는 다음과 같습니다.

protected ConsList() {
    Class<?> clazz = getClass();
    if (clazz != Empty.class && clazz != ConsCell.class) throw new Exception();
}

C #에서는 구체화 된 제네릭 때문에 더 복잡합니다. 가장 간단한 방법은 유형을 문자열로 변환하고 엉망으로 만드는 것입니다.

Java에서는이 메커니즘을 이론적으로 직렬화 모델 또는을 통해 실제로 원하는 사람이 우회 할 수 있습니다 sun.misc.Unsafe.


1
C #에서는 더 복잡하지 않습니다 :Type type = this.GetType(); if (type != typeof(Empty<T>) && type != typeof(ConsCell<T>)) throw new Exception();
svick

@svick, 잘 관찰했다. 기본 유형이 매개 변수화되는 것을 고려하지 않았습니다.
피터 테일러

훌륭한! "수동 정적 유형 검사"를 수행하기에 충분하다고 생각합니다. 나는 악의적 인 의도보다는 정직한 프로그래밍 오류를 제거하려고합니다.
Jörg W Mittag
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.