Что построят конструкторы Haskell (data)?

Haskell позволяет строить алгебраические типы данных с использованием конструкторов типов и конструкторов данных. Например,

data Circle = Circle Float Float Float

и нам говорят, что этот конструктор данных (Circle справа) - это функция, которая строит круг при предоставлении данных, например x, y, radius.

Circle :: Float -> Float -> Float -> Circle 

Мои вопросы:

  1. Что конкретно построено этой функцией, в частности?

  2. Можем ли мы определить функцию конструктора?

Я видел Smart Constructors, но они просто кажутся дополнительными функциями, которые в конечном итоге называют регулярными конструкторами.

Исходя из фона OO, конструкторы, конечно же, имеют императивные спецификации. В Haskell они кажутся системными.

Ответы

Ответ 1

В Haskell, без учета базовой реализации, конструктор данных создает значение, по существу, с помощью fiat. "Да будет Circle ", сказал программист, и был Circle ". Спрашивать, что создает Circle 1 2 3 равно что спрашивать, что создает литерал 1 в Python или Java.

Нулевой конструктор ближе к тому, что вы обычно считаете литералом. Boolean тип буквально определяется как

data Boolean = True | False

где True и False являются конструкторами данных, а не литералами, определенными грамматикой Haskell.

Тип данных также является определением конструктора; поскольку на самом деле в значении, кроме имени конструктора и его аргументов, нет ничего, просто указать, что это определение. Вы создаете значение типа Circle, вызывая конструктор данных Circle с 3 аргументами, и это все.

Так называемый "умный конструктор" - это просто функция, которая вызывает конструктор данных, возможно, с некоторой другой логикой для ограничения того, какие экземпляры могут быть созданы. Например, рассмотрим простую обертку вокруг Integer:

newtype PosInteger = PosInt Integer

Конструктор PosInt; умный конструктор может выглядеть

mkPosInt :: Integer -> PosInteger
mkPosInt n | n > 0 = PosInt n
           | otherwise = error "Argument must be positive"

С mkPosInt невозможно создать значение PosInteger с неположительным аргументом, потому что только положительные аргументы фактически вызывают конструктор данных. Интеллектуальный конструктор имеет смысл, когда он, а не конструктор данных, экспортируется модулем, поэтому обычный пользователь не может создавать произвольные экземпляры (поскольку конструктор данных не существует вне модуля).

Ответ 2

Хороший вопрос. Как вы знаете, учитывая определение:

data Foo = A | B Int

это определяет тип с конструктором типа (nullary) типа Foo и двумя конструкторами данных A и B

Каждый из этих конструкторов данных, когда они полностью применяются (без аргументов в случае A и одного аргумента Int в случае B), строит значение типа Foo. Итак, когда я пишу:

a :: Foo
a = A

b :: Foo
b = B 10

имена a и b связаны с двумя значениями типа Foo.

Итак, конструкторы данных для типа Foo строят значения типа Foo.

Каковы значения типа Foo? Ну, во-первых, они отличаются от значений любого другого типа. Во-вторых, они полностью определяются их конструкторами данных. Существует различное значение типа Foo, отличное от всех других значений Foo, для каждой комбинации конструктора данных с набором различных аргументов, переданных этому конструктору данных. То есть два значения типа Foo идентичны тогда и только тогда, когда они были сконструированы с одним и тем же конструктором данных с идентичными наборами аргументов. ("Идентичность" здесь означает нечто отличное от "равенства", которое необязательно может быть определено для данного типа Foo, но не вдаваться в это.)

Это также делает конструкторы данных отличными от функций в Haskell. Если у меня есть функция:

bar :: Int -> Bool

Возможно, что bar 1 и bar 2 могут быть точно одинаковыми. Например, если bar определяется:

bar n = n > 0

то очевидно, что bar 1 и bar 2bar 3) идентичны True. Является ли значение bar одинаковым для разных значений его аргументов, будет зависеть от определения функции.

Напротив, если Bar является конструктором:

data BarType = Bar Int

то никогда не будет так, что Bar 1 и Bar 2 будут одинаковыми. По определению они будут разными значениями (типа BarType).

Кстати, идея о том, что конструкторы - это просто особый вид функции, является общей точкой зрения. Я лично считаю, что это неточно и вызывает путаницу. Хотя верно, что конструкторы часто могут использоваться так, как если бы они были функциями (в частности, они очень похожи на функции при использовании в выражениях), я не думаю, что это мнение стоит под большим вниманием - конструкторы представлены по-разному на поверхности синтаксис языка (с заглавными идентификаторами), может использоваться в контекстах (например, сопоставление с образцом), где функции не могут использоваться, представлены по-разному в скомпилированном коде и т.д.

Итак, когда вы спрашиваете "можем ли мы определить функцию конструктора", ответ "нет", потому что нет функции-конструктора. Вместо этого конструктор, такой как A или B или Bar или Circle является тем, чем он является - чем-то отличается от функции (которая иногда ведет себя как функция с некоторыми дополнительными свойствами), которая способна построить значение любого типа, к которому принадлежит конструктор данных к.

Это делает конструкторы Haskell очень отличными от конструкторов OO, но это не удивительно, поскольку значения Haskell сильно отличаются от OO-объектов. На языке OO вы обычно можете предоставить функцию-конструктор, которая выполняет некоторую обработку при создании объекта, поэтому в Python вы можете написать:

class Bar:
    def __init__(self, n):
        self.value = n > 0

а затем после:

bar1 = Bar(1)
bar2 = Bar(2)

у нас есть два разных объекта bar1 и bar2 (которые будут насыщать bar1 != bar2), которые были настроены с одинаковыми значениями поля и в некотором смысле "равны". Это примерно на полпути между ситуацией выше с bar 1 и bar 2 создающим два идентичных значения (а именно True), и ситуация с Bar 1 и Bar 2 создающая два разных значения, которые по определению не могут быть "одинаковыми", в любом смысле.

Вы никогда не можете иметь эту ситуацию с конструкторами Haskell. Вместо того, чтобы думать о конструкторе Haskell как о запуске некоторой базовой функции для "создания" объекта, который может включать в себя некоторую классную обработку и получение значений поля, вместо этого вы должны думать о конструкторе Haskell как пассивном теге, прикрепленном к значению (что также может содержат ноль или более других значений, в зависимости от арности конструктора).

Итак, в вашем примере Circle 10 20 5 не "конструирует" объект типа Circle, запустив некоторую функцию. Он непосредственно создает объект с тегами, который в памяти будет выглядеть примерно так:

<Circle tag>
<Float value 10>
<Float value 20>
<Float value 5>

(или вы можете хотя бы сделать вид, что это похоже на память).

Ближе всего вы можете прийти к конструкторам OO в Haskell, используя интеллектуальные конструкторы. Как вы заметили, в конечном итоге интеллектуальный конструктор просто вызывает обычный конструктор, потому что это единственный способ создать значение данного типа. Независимо от того, какой необычный умный конструктор вы создаете для создания Circle, значение, которое он создает, должно выглядеть следующим образом:

<Circle tag>
<some Float value>
<another Float value>
<a final Float value>

который вам нужно будет построить с помощью простого старого вызова конструктора Circle. Ничто другое, что мог бы вернуть умный конструктор, все равно будет Circle. Вот как работает Haskell.

Это помогает?

Ответ 3

Я собираюсь ответить на это несколько обходным путем, на примере, который, я надеюсь, иллюстрирует мою мысль, а именно, что Haskell отделяет несколько различных идей, которые связаны в ООП под понятием "класс". Понимание этого поможет вам с трудом перевести свой опыт с ООП в Хаскелл. Пример в псевдокоде ООП:

class Person {

    private int id;
    private String name;

    public Person(int id, String name) {
        if (id == 0)
            throw new InvalidIdException();
        if (name == "")
            throw new InvalidNameException();

        this.name = name;
        this.id = id;
    }

    public int getId() { return this.id; }

    public String getName() { return this.name; }

    public void setName(String name) { this.name = name; }

}

В Haskell:

module Person
  ( Person
  , mkPerson
  , getId
  , getName
  , setName
  ) where

data Person = Person
  { personId :: Int
  , personName :: String
  }

mkPerson :: Int -> String -> Either String Person
mkPerson id name
  | id == 0 = Left "invalid id"
  | name == "" = Left "invalid name"
  | otherwise = Right (Person id name)

getId :: Person -> Int
getId = personId

getName :: Person -> String
getName = personName

setName :: String -> Person -> Either String Person
setName name person = mkPerson (personId person) name

Обратите внимание:

  • Класс Person был переведен в модуль, который экспортирует тип данных с помощью тех же именных типов (для представления и инвариантов домена), которые отделяются от модулей (для размещения имен и организации кода).

  • Поля id и name, которые определяются как private в class определения, переводятся в обычные (общественных) полей на data определения, поскольку в Haskell Theyre личными, опуская их из списка экспорта Person модуля-определений и видимости развязаны.

  • Конструктор был переведен на две части: один (конструктор данных Person), который просто инициализирует поля, а другой (mkPerson), который выполняет выделение, инициализацию и проверку валидации, развязаны. Поскольку тип Person экспортируется, но его конструктор не является, это единственный способ для клиентов создать Person - это "абстрактный тип данных".

  • Публичный интерфейс был переведен на функции, которые экспортируются модулем Person, а функция setName которая ранее мутировала объект Person, стала функцией, которая возвращает новый экземпляр типа данных Person который позволяет разделить старый идентификатор. Код OOP имеет ошибку: он должен включать проверку setName для name != "" Инвариант; код Haskell может избежать этого, используя интеллектуальный конструктор mkPerson чтобы гарантировать, что все значения Person действительны по построению. Таким образом, переходы и проверка состояния также развязаны - вам нужно только проверять инварианты при построении значения, потому что он не может измениться после этого.

Что касается ваших актуальных вопросов:

  1. Что конкретно построено этой функцией, в частности?

Конструктор типа данных выделяет пространство для тега и полей значения, устанавливает тег, к которому был использован конструктор для создания значения, и инициализирует поля аргументами конструктора. Вы не можете отменить это, потому что процесс полностью механический, и для этого нет причин (в нормальном безопасном коде). Его внутренняя деталь языка и времени исполнения.

  1. Можем ли мы определить функцию конструктора?

Нет. Если вы хотите выполнить дополнительную проверку для принудительного применения инвариантов, вы должны использовать функцию "умный конструктор", которая вызывает конструктор данных нижнего уровня. Поскольку значения Haskell по умолчанию неизменяемы, значения могут быть сделаны корректными по построению; то есть, когда у вас нет мутации, вам не нужно обеспечивать, чтобы все переходы состояния были правильными, только чтобы все состояния были построены правильно. И часто вы можете упорядочить свои типы, чтобы интеллектуальные конструкторы даже были необходимы.

Единственное, что вы можете изменить о сгенерированной "конструктор" конструктора данных, делает его сигнатуру типа более ограничительной с использованием GADT, чтобы помочь обеспечить большее количество инвариантов во время компиляции. И в качестве дополнительной заметки GADT также позволяют делать экзистенциальную квантификацию, которая позволяет переносить информацию с инкапсулированным/стираемым стилем во время выполнения, точно так же, как VOP vtable, поэтому это еще одна вещь, которая отделяется в Haskell, но связана с типичными языками ООП.

Короче говоря (слишком поздно), вы можете делать все то же самое, вы просто упорядочиваете их по-другому, потому что Haskell предоставляет различные функции классов ООП под отдельными функциями ортогонального языка.