Случаи типов и полей записей в Haskell

Два следующих фрагмента кода выглядят очень похожими. Однако должны быть некоторые различия, и я надеюсь, что кто-то может указать на них.

data Animal = Cat | Dog
speak :: Animal -> String
speak Cat = "meowh"
speak Dog = "wouf"

и

data Animal = Animal { speak :: String }
cat = Animal { speak = "meowh"}
dog = Animal { speak = "wouf" }

Ответы

Ответ 1

Хороший вопрос! Вы поразили суть одной из фундаментальных проблем разработки программного обеспечения. Per Wadler, он называл "Проблема выражения" , и, примерно подведя итог, вопрос:

Легко ли добавлять новые операции или новые типы данных?

Ваш первый пример позволяет легко добавлять новые операции к существующим животным. Мы можем обрабатывать значения Animal различными способами без изменения определения Animal:

numberOfTeeth :: Animal -> Int
numberOfTeeth Cat = 30
numberOfTeeth Dog = 42

food :: Animal -> String
food Cat = "Fish"
food Dog = "Sausages"  -- probably stolen from a cartoon butcher

Недостатком является то, что трудно добавлять новые типы животных. Вы должны добавить новые конструкторы в Animal и изменить все существующие операции:

data Animal = Cat | Dog | Crocodile

speak :: Animal -> String
speak Cat = "miaow"
speak Dog = "woof"
speak Crocodile = "RAWR"

numberOfTeeth :: Animal -> Int
numberOfTeeth Cat = 30
numberOfTeeth Dog = 42
numberOfTeeth Crocodile = 100000  -- I'm not a vet

food :: Animal -> String
food Cat = "Fish"
food Dog = "Sausages"
food Crocodile = "Human flesh"

Ваш второй пример переворачивает матрицу, делая ее легкой для добавления новых типов,

crocodile = Animal { speak = "RAWR" }

но трудно добавить новые функции - это означает добавление новых полей в Animal и обновление всех существующих животных.

data Animal = Animal {
    speak :: String,
    numberOfTeeth :: Int,
    food :: String
}
cat = Animal {
    speak = "miaow",
    numberOfTeeth = 30,
    food = "Fish"
}
dog = Animal {
    speak = "woof",
    numberOfTeeth = 42,
    food = "Sausages"
}
crocodile = Animal {
    speak = "RAWR",
    numberOfTeeth = 100000,
    food = "Human flesh"
}

Не стоит недооценивать, насколько велика сделка, проблема выражения! Если вы работаете над опубликованной библиотекой, вы можете столкнуться с операциями или типами, определенными кем-то, кого вы никогда не встречали в кодовой базе, которую вы не можете изменить. Вы должны тщательно подумать о том, как вы ожидаете, что люди будут использовать вашу систему, и решите, как ориентировать дизайн вашей библиотеки.

С годами умные люди изобрели множество умных способов решения проблемы выражения, для поддержки новых операций и новых типов. Эти решения, как правило, сложны, используя самые передовые функции самых современных языков программирования. В реальном мире это просто еще один компромисс, который нужно учитывать - решает проблему с выражением, стоящую за сложность кода, которую это вызовет?

Ответ 2

Сначала давайте присваиваем типам имена, которые ближе отражают их суть:

data AnimalType =
  Cat | Dog

newtype Phrase =
  Phrase { phrase :: String }

Уже становится очевидным, что они очень разные и явно изолируемы.

Фраза вот путь более общий. Это могут быть произнесены не только животными, но и роботами по крайней мере. Но тем более это можно не только произнести, но и трансформировать с помощью операций, таких как верхний корпус и т.д. Такие операции не имеют смысла для типа AnimalType.

AnimalType OTOH имеет свои преимущества. Подбирая тип, вы можете выбрать тип пищи, которая необходима животному, или ее размер, и т.д. Вы не можете сделать это на Phrase.

Вы также можете использовать оба типа друг для друга в своем приложении и иметь преобразование (и, следовательно, зависимость) от более конкретного к более общему:

module Animal where

import qualified Phrase

speakPhrase :: Animal -> Phrase.Phrase
speakPhrase =
  Phrase.Phrase . speak

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

Ответ 3

Первый фрагмент - это то, что мы называем замкнутым. Это означает, что он не расширяется, а второй фрагмент: мы можем создать столько объектов Animal, которые нам нравятся, не ограничиваясь Cat и Dog.

Но вы правы, это очень похоже на то, как мы его используем, за исключением соответствия шаблону. Вы можете сопоставить образец с первым фрагментом, но не со вторым. Однако это не проблема, если вы вывести второе определение Animal из класса Eq для сравнения с Cat и Dog.