Есть некоторые проблемы, которые легко решаются алгебраическими типами данных, например, тип List может быть очень кратко выражен как:
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, но в других языках он будет аналогичным с нативной поддержкой алгебраических типов данных.
Оказывается, существует очевидное сопоставление с подтипами в стиле ОО: тип данных становится абстрактным базовым классом, а каждый конструктор данных становится конкретным подклассом. Вот пример в Scala:
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? Или, если это невозможно, какой будет идиоматическая замена?
Ответы:
Существует простой, но сложный способ герметизации классов в Java. Вы помещаете частный конструктор в базовый класс, а затем создаете его подклассы.
Прикрепите шаблон посетителя для отправки.
Мой проект jADT: Java Algebraic DataTypes генерирует все эти шаблоны для вас https://github.com/JamesIry/jADT
источник
Either
. Смотрите мой вопросВы можете добиться этого с помощью шаблона посетителя , который будет дополнять сопоставление с шаблоном. Например
может быть написано на Java как
Запечатывание достигается
Visitor
классом. Каждый из его методов объявляет, как деконструировать один из подклассов. Вы могли бы добавить больше подклассов, но это должно было бы быть реализованоaccept
и путем вызова одного изvisit...
методов, так что он должен был бы вести себя какCons
или какNil
.источник
Если вы злоупотребляете именованными параметрами C # (введено в C # 4.0), вы можете создавать алгебраические типы данных, которые легко сопоставить:
Вот реализация
Either
класса:источник
class Right<R> : Either<Bot,R>
где Either изменен на интерфейс с ковариантными (out) параметрами типа, а Bot является нижним типом (подтип любого другого типа, противоположный Object). Я не думаю, что C # имеет нижний тип.В C # этот
Empty
тип не может быть , потому что из-за reification базовые типы различаются для разных типов элементов. Вы можете только иметьEmpty<T>
; не так полезноВ Java это может произойти
Empty : ConsList
из-за стирания типа, но я не уверен, что средство проверки типов не будет кричать где-нибудь.Тем не менее, поскольку оба языка имеют
null
, вы можете думать о всех их ссылочных типах как о «Безотносительно | Нуль». Таким образом, вы просто использовали быnull
как «Пустой», чтобы не указывать, что он получает.источник
null
том, что он слишком общий: он представляет отсутствие чего-либо , то есть пустоту вообще, но я хочу представить отсутствие элементов списка, то есть, в частности, пустой список. Пустой список и пустое дерево должны иметь разные типы. Кроме того, пустой список должен быть фактическим значением, потому что он все еще имеет свое собственное поведение, поэтому он должен иметь свои собственные методы. Чтобы построить список[1, 2, 3]
, я хочу сказатьEmpty.prepend(3).prepend(2).prepend(1)
(или на языке с правоассоциативными операторами1 :: 2 :: 3 :: Empty
), но я не могу сказатьnull.prepend …
.Empty
and и an,Empty<>
чтобы разрешить довольно практичное моделирование, если хотите. По сути, вы используетеEmpty
в коде, но все сигнатуры типов и т. Д. Используют только общие варианты.В Java вы не можете. Но вы можете объявить базовый класс как частный пакет, что означает, что все прямые подклассы должны принадлежать тому же пакету, что и базовый класс. Если вы затем объявите подклассы как окончательные, они не могут быть разделены на подклассы.
Я не знаю, решит ли это вашу настоящую проблему, хотя ...
источник
intanceof
проверку «псевдобезопасного типа» (то есть: я знаю, что это безопасно, даже если компилятор этого не делает), просто гарантируя, что я всегда проверьте эти два случая. Однако, если кто-то еще добавляет новый подкласс, тогда я могу получить ошибки времени выполнения, которых я не ожидал.Тип данных
ConsList<A>
может быть представлен как интерфейс. Интерфейс предоставляет единственныйdeconstruct
метод, который позволяет вам «деконструировать» значение этого типа, то есть обрабатывать каждый из возможных конструкторов. Вызовdeconstruct
метода аналогиченcase of
форме в Haskell или ML.deconstruct
Метод принимает функцию «обратного вызова» для каждого конструктора в ADT. В нашем случае, она принимает функцию для случая пустого списка и другую функцию для случая "cons-ячейки".Каждая функция обратного вызова принимает в качестве аргументов значения, которые принимаются конструктором. Таким образом, случай «пустой список» не принимает аргументов, а случай «cons cell» принимает два аргумента: заголовок и конец списка.
Мы можем закодировать эти «множественные аргументы», используя
Tuple
классы или используя карри. В этом примере я решил использовать простойPair
класс.Интерфейс реализован один раз для каждого конструктора. Во-первых, у нас есть реализация для «пустого списка».
deconstruct
Реализация просто вызываетemptyCase
функцию обратного вызова.Затем мы реализуем случай «cons клетка» аналогично. На этот раз у класса есть свойства: голова и хвост непустого списка. В
deconstruct
реализации эти свойства передаются вconsCase
функцию обратного вызова.Вот пример использования этой кодировки ADT: мы можем написать
reduce
функцию, которая является обычным сворачиванием списков.Это аналогично этой реализации в Haskell:
источник
Нет хорошего способа сделать это, но если вы хотите жить с отвратительным взломом, вы можете добавить некоторую явную проверку типов в конструктор абстрактного базового класса. В Java это было бы что-то вроде
В C # это более сложно из-за усовершенствованных обобщений - самый простой подход может состоять в том, чтобы преобразовать тип в строку и исправить это.
Обратите внимание, что в Java даже этот механизм теоретически может быть обойден кем-то, кто действительно хочет через модель сериализации или
sun.misc.Unsafe
.источник
Type type = this.GetType(); if (type != typeof(Empty<T>) && type != typeof(ConsCell<T>)) throw new Exception();