Cebirsel Veri Tiplerini C # - veya Java benzeri bir dilde nasıl kodlarsınız?


58

Cebirsel Veri Tipleri ile kolayca çözülebilen bazı problemler vardır, örneğin bir Liste tipi kısaca şöyle ifade edilebilir:

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

Bu özel örnek Haskell'dedir, ancak Cebirsel Veri Türleri için yerel desteği olan diğer dillerde benzer olacaktır.

OO tarzı alt tipleme için açık bir eşleme olduğu ortaya çıktı: veri türü soyut bir temel sınıf haline geldi ve her veri yapıcısı somut bir alt sınıf haline geldi. İşte Scala'da bir örnek:

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+)

Naif subclassing ötesinde gerekli tek şey bir yoludur mühür imkansız bir hiyerarşiye alt sınıfları eklemek yapmak için bir yol, yani sınıflar.

Bu soruna C # veya Java gibi bir dilde nasıl yaklaşırsınız? C # 'da Cebirsel Veri Tiplerini kullanmaya çalışırken bulduğum iki engel:

  • C # da alt tipin ne dendiğini anlayamadım (yani ne yazacağımı bulamadım class Empty : ConsList< ??? >)
  • Hiyerarşiye hiçbir alt sınıf eklenememesi için mühürlemenin bir yolunu bulamadım.ConsList

Cebirsel Veri Tiplerini C # ve / veya Java ile uygulamanın en aptalca yolu ne olabilir? Ya da mümkün değilse, deyimsel ikame ne olurdu?


4
İlgi alanı: C #
AakashM

3
C #, OOP dilidir. OOP kullanarak problemleri çözün. Başka bir paradigma kullanmayı denemeyin.
Öforik,

7
@Euphoric C #, C # 3.0 ile oldukça kullanışlı bir fonksiyonel dil haline geldi. Birinci sınıf fonksiyonlar, yerleşik ortak işlevsel işlemler, monadlar.
Mauricio Scheffer

2
@Euphoric: Bazı alanlar cisimlerle modellemek kolaydır ve cebirsel veri tipleri ile modellemek zordur, bazıları tam tersidir. Her ikisinin de nasıl yapıldığını bilmek, etki alanınızı modellemede size daha fazla esneklik sağlar. Ve dediğim gibi, cebirsel veri tiplerini tipik OO kavramlarıyla eşleştirmek o kadar da karmaşık değildir: veri tipi soyut bir temel sınıf (veya bir arayüz veya soyut bir özellik) haline gelir, veri yapıcılar somut uygulama alt sınıfları haline gelir. Bu size açık bir cebirsel veri türü verir. Miras kısıtlamaları size kapalı bir cebirsel veri türü verir. Polimorfizm size davada ayrımcılık verir.
Jörg W Mittag

3
@Ephoric, paradigma, schmaradigm, kimin umrunda? ADT'ler fonksiyonel programlamaya (veya OOP veya başka bir şekilde) diktir. Herhangi bir dilin AST'sini kodlamak, ADT'lerin nezih desteği olmadan oldukça acı vericidir ve bu dilin derlenmesi, başka bir paradigma-agnostik özelliği olmayan desen eşleştirmesi olan bir acıdır.
SK-mantığı

Yanıtlar:


42

Java'da sınıfları mühürlemenin kolay, ancak kaynatıcı bir yolu var. Temel sınıfa özel bir kurucu koyarsınız, daha sonra bunun alt sınıflarını yaparsınız.

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;
      }
   }
}

Göndermek için bir ziyaretçi kalıbına dokun.

Projem jADT: Java Cebirsel DataTypes sizin için bütün bu kazanları üretir https://github.com/JamesIry/jADT


2
Her nasılsa isminin burada ortaya çıkmasına şaşırmam! Teşekkürler, bu deyimi bilmiyordum.
Jörg W Mittag

4
"Ağır levha kazan" derken çok daha kötü bir şey için hazırlanmıştım ;-) Java bazen sıcak levha ile oldukça kötü olabilir.
Joachim Sauer

ancak bu oluşmaz: A türünü bir alçıyla iddia etmek zorunda kalmadan uzmanlaşmanın bir yolu yok (sanırım)
nicolas

Bu ne yazık ki, örneğin daha karmaşık toplam türlerini temsil edemiyor gibi görünüyor Either. Bkz sorumu
Zoey Hewll

20

Desen eşleşmesini tamamlayacak ziyaretçi desenini kullanarak bunu başarabilirsiniz . Örneğin

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

Java’da olduğu gibi yazılabilir.

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);
    }
}

Sızdırmazlık Visitorsınıfı tarafından sağlanır . Yöntemlerinin her biri, alt sınıflardan birinin nasıl yapılacağını açıklar. Daha alt sınıfları ekleyebilirsiniz, ancak uygulamak zorunda kalacak acceptve birini çağırarak visit...yöntemlerle, bu nedenle gibi davranmaya olurdu ya Consya ister Nil.


13

C # isimli parametreleri kötüye kullanırsanız (C # 4.0'da tanıtılır), eşleşmesi kolay cebirsel veri türlerini yapabilirsiniz:

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);

İşte Eithersınıfın uygulaması :

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);
    }
}

Daha önce bu tekniğin bir Java sürümünü gördüm, ancak lambdas ve adlandırılmış parametreler onu çok okunaklı hale getiriyor. 1!
Doval

1
Bence buradaki problem, Hak'ın hata türüne göre genel olmadığıdır. Şunun gibi bir şey: ( class Right<R> : Either<Bot,R>Her ikisinin de kovariant (out) type parametreleriyle bir arabirime değiştirildiği ve Bot, en alttaki türdür (Object'in tersine, diğer her türün alt türü). C # 'nın alt tipi olduğunu sanmıyorum.
croyd

5

C # 'da, o Emptytipe sahip olamazsınız , çünkü, yeniden birleşme nedeniyle, temel tipler, farklı üye tipleri için farklıdır. Sadece sahip olabilirsiniz Empty<T>; o kadar kullanışlı değil.

Java'da Empty : ConsListtür silme nedeniyle olabilirsiniz , ancak tür denetleyicisinin bir yerde çığlık atmayacağından emin değilim.

Ancak null, her iki dilde de bulunduğundan , tüm referans türlerini "Whatever | Null" olarak düşünebilirsiniz . Yani nullne türettiğini belirtmek zorunda kalmamak için sadece “Boş” olarak kullanırsınız.


Sorun nullşu ki, bu çok genel: bir şeyin yokluğunu , yani boşluğu temsil ediyor. genel olarak temsil ediyor, ancak liste öğelerinin yokluğunu, özellikle de boş bir listeyi temsil etmek istiyorum. Boş bir liste ve boş bir ağaç farklı tiplerde olmalıdır. Ayrıca, boş listenin gerçek bir değer olması gerekir, çünkü yine de kendi davranışına sahiptir, bu nedenle kendi yöntemlerine sahip olması gerekir. Listeyi oluşturmak [1, 2, 3]için şunu söylemek istiyorum Empty.prepend(3).prepend(2).prepend(1)(veya doğru ilişkilendirme operatörleri olan bir dilde 1 :: 2 :: 3 :: Empty), ancak söyleyemem null.prepend ….
Jörg W Mittag

@ JörgWMittag: Boş değerlerin farklı türleri vardır. Ayrıca, amaç için null değeriyle kolayca yazılmış bir sabit oluşturabilirsiniz. Ama bunun için yöntem çağıramazsınız. Yöntemlere olan yaklaşımınız, yine de öğeye özgü Boş bırakmadan çalışmaz.
Jan Hudec,

Bazı ustalık uzantısı yöntemleri sahte 'yöntemi' boş çağrıları (elbette hepsi gerçekten statik) çağrıları olabilir
jk.

Bir olabilir Emptyve bir Empty<>ve isterseniz, oldukça pratik bir simülasyon sağlamak için örtülü dönüşüm operatörleri kötüye. Temel olarak, Emptykodda kullanırsınız , ancak tüm imzalar vb. Yalnızca genel değişkenleri kullanır.
Eamon Nerbonne

3

Saf alt sınıflamanın ötesinde ihtiyaç duyulan tek şey, sınıfları kapatmanın bir yolu, yani bir hiyerarşiye alt sınıfları eklemeyi imkansız hale getirmenin bir yoludur.

Java'da yapamazsın. Ancak, temel sınıfı özel paket olarak ilan edebilirsiniz; bu, tüm doğrudan alt sınıfların, temel sınıfla aynı pakete ait olması gerektiği anlamına gelir. Alt sınıfları nihai olarak ilan ederseniz, daha fazla alt sınıflandırılamazlar.

Bunun senin gerçek problemini çözüp çözmeyeceğini bilmiyorum ...


Gerçek bir problemim yok, yoksa bunu StackOverflow'a gönderirdim, burada değil :-) Cebirsel Veri Tiplerinin önemli bir özelliği kapalı olmalarıdır , yani vaka sayısının sabit olduğu anlamına gelir: bu örnekte , bir liste ya boş ya da değil. Durumun böyle olmasını statik olarak sağlayabilirsem, dinamik diskler veya intanceof"sahte türden güvenli" dinamik kontroller yapabilirim (yani: derleyici olmasa bile güvenli olduğunu biliyorum) şu iki vakayı kontrol et. Bununla birlikte, başka biri yeni bir alt sınıf eklerse, beklediğim çalışma zamanı hataları alabilirim.
Jörg W Mittag

@ JörgWMittag - Peki, Java açıkça ... desteklediğini düşündüğün gibi desteklemiyor. Tabii ki, çalışma zamanında istenmeyen alt yazmayı engellemek için çeşitli şeyler yapabilirsiniz, ancak daha sonra "beklediğiniz çalışma zamanı hataları" alırsınız.
Stephen C

3

Veri tipi ConsList<A>bir arayüz olarak gösterilebilir. Arabirim deconstruct, bu tür bir değeri "yapıyı çıkarmanıza" olanak tanıyan - yani olası yapıcıların her birini işlemek için tek bir yöntem sunar. Bir deconstructyönteme yapılan çağrılar case ofHaskell veya ML'deki bir forma benzer .

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

deconstructYöntem ADT her yapıcısı için bir "geri arama" fonksiyonu alır. Bizim durumumuzda boş liste durumu için bir fonksiyon ve "cons cell" durumu için başka bir fonksiyon alır.

Her geri çağırma işlevi, yapıcı tarafından kabul edilen değerleri argüman olarak kabul eder. Böylece "boş liste" durumu argüman almaz, ancak "cons cell" durumu iki argüman alır: listenin başı ve kuyruğu.

Bu "çoklu argümanları" Tuplesınıfları kullanarak veya körleştirerek kodlayabiliriz . Bu örnekte basit bir kullanmayı tercih ettim.Pair sınıf .

Arabirim, her yapıcı için bir kez uygulanır. İlk olarak, "boş liste" için uygulamaya sahibiz. deconstructUygulama basitçe çağırır emptyCasegeri arama işlevi.

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());
  }
}

Sonra da "eksileri hücre" vakasını benzer şekilde uygularız. Bu kez sınıfın özellikleri vardır: boş olmayan listenin başı ve kuyruğu. In deconstructuygulanması, bu özellikler geçirilir consCasegeri arama işlevi.

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));
  }
}

İşte ADT'lerin bu kodlamasını kullanmaya bir örnek: reducelistelerde normal katlama olan bir fonksiyon yazabiliriz .

<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))
  );
}

Bu, Haskell'deki bu uygulamaya benzer:

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

İlginç yaklaşım, çok güzel! F # Aktif Modeller ve Scala Extractors ile bağlantısını görebiliyorum (ve muhtemelen orada Haskell Views ile ilgili bir şey bilmediğim bir bağlantı da var). Veri kurucuları üzerinden örüntü eşleme sorumluluğunu ADT örneğinin kendisine taşımayı düşünmemiştim.
Jörg W Mittag

2

Saf alt sınıflamanın ötesinde ihtiyaç duyulan tek şey, sınıfları kapatmanın bir yolu, yani bir hiyerarşiye alt sınıfları eklemeyi imkansız hale getirmenin bir yoludur.

Bu soruna C # veya Java gibi bir dilde nasıl yaklaşırsınız?

Bunu yapmanın iyi bir yolu yoktur, ancak çirkin bir hack ile yaşamaya istekli iseniz, soyut temel sınıfın kurucusuna bazı açık tip kontroller ekleyebilirsiniz. Java’da, bunun gibi bir şey olurdu

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

C # 'da, birleşmiş jenerikler nedeniyle daha karmaşıktır - en basit yaklaşım, türü bir dizgeye dönüştürmek ve onu karıştırmak olabilir.

Java'da bu mekanizmanın bile seri hale getirme modeli veya yoluyla gerçekten isteyenler tarafından teorik olarak atlanabileceğini unutmayın sun.misc.Unsafe.


1
C #:Type type = this.GetType(); if (type != typeof(Empty<T>) && type != typeof(ConsCell<T>)) throw new Exception();
svick

@svick, iyi gözlenmiş. Temel türün parametreleneceğini dikkate almıyordum.
Peter Taylor

Parlak! Sanırım bu "el ile statik tip kontrolü" yapmak için yeterince iyi. Kötü niyetli bir niyetten ziyade dürüst programlama hatalarını ortadan kaldırmak istiyorum.
Jörg W Mittag
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.