Значения по умолчанию в типах данных Haskell

когда вы определяете класс на объектно-ориентированном языке, он обычно устанавливает значения по умолчанию для переменных-членов. Есть ли какой-либо механизм в Haskell, чтобы сделать то же самое в типах записей? И следующий вопрос: если мы с самого начала не знаем всех значений для конструктора данных, но получаем их из взаимодействия IO, можем ли мы построить тип, используя что-то вроде шаблона builder из OOP?

спасибо заранее

3 ответов


общей идиомой является определение значения по умолчанию.

data A = A { foo :: Int , bar :: String }

defaultA :: A
defaultA = A{foo = 0, bar = ""}

это может быть затем (чисто)" обновлено " позже с реальными значениями.

doSomething :: Bool -> A
doSomething True  = defaultA{foo = 32}
doSomething False = defaultA{bar = "hello!"}

псевдокод пример:

data Options = O{ textColor :: Bool, textSize :: Int, ... }

defaultOptions :: Options
defaultOptions = O{...}

doStuff :: Options -> IO ()
doStuff opt = ...

main :: IO ()
main = do
     ...
     -- B&W, but use default text size
     doStuff defaultOptions{ color = False }

если нет разумных значений по умолчанию, вы можете обернуть значения полей в Maybe.

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


есть ли какой-либо механизм в Haskell, чтобы сделать то же самое в типах записей?

что вы можете сделать, это скрыть конструктор и предоставить функцию в качестве конструктора вместо.

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

data RevisionList a = RevisionList { theList :: [a],
                                     revision :: Int }
                          deriving Show

теперь мы можем определить функцию, которая инициализирует BuildList с начальной список:

revisionList :: [a] -> RevisionList a
revisionList xs = RevisionList { theList = xs, revision=0 }

и скрывая конструктор в module экспорт, таким образом, мы скрываем возможность инициализации его с другой ревизией, чем ревизия 0. Таким образом, модуль может выглядеть так:

module Foo(RevisionList(), revisionList)

data RevisionList a = RevisionList { theList :: [a],
                                     revision :: Int }

revisionList :: [a] -> RevisionList a
revisionList xs = RevisionList { theList = xs, revision=0 }

что-то вроде шаблона от ООП?

мы можем, например, использовать State монада за это. Например:

module Foo(RevisionList(), revisionList,
           increvision, RevisionListBuilder, prefixList)

import Control.Monad.State.Lazy

type RevisionListBuilder a = State (RevisionList a)

increvision :: RevisionListBuilder a ()
increvision = do
    rl <- get
    put (rl { revision = 1 + revision rl})

prefixList :: a -> RevisionListBuilder a ()
prefixList x = do
    rl <- get
    put (rl { theList = x : theList rl })
    increvision

мы get the RevisionList до сих пор, выполнять обновления, а put в новый результат вернулся.

теперь другой модуль может импортировать наши Foo, и используйте строитель как:

some_building :: RevisionListBuilder Int ()
some_building = do
    prefixList 4
    prefixList 1

и теперь мы можем "сделать"RevisionList в редакции 2 с окончательным списком [1,4,2,5] С:

import Control.Monad.State.Lazy(execState)

some_rev_list :: RevisionList Int
some_rev_list = execState some_building (revisionList [2,5])

так это будет выглядеть примерно так:

Foo.hs:

module Foo(RevisionList(), revisionList,
           increvision, RevisionListBuilder, prefixList)

data RevisionList a = RevisionList { theList :: [a],
                                     revision :: Int }
                          deriving Show
type RevisionListBuilder a = State (RevisionList a)

revisionList :: [a] -> RevisionList a
revisionList xs = RevisionList { theList = xs, revision=0 }

increvision :: RevisionListBuilder a ()
increvision = do
    rl <- get
    put (rl { revision = 1 + revision rl})

prefixList :: a -> RevisionListBuilder a ()
prefixList x = do
    rl <- get
    put (rl { theList = x : theList rl })
    increvision

Bar.hs:

import Foo
import Control.Monad.State.Lazy(execState)

some_building :: RevisionListBuilder Int ()
some_building = do
    prefixList 4
    prefixList 1

some_rev_list :: RevisionList Int
some_rev_list = execState some_building (revisionList [2,5])

Итак, теперь мы построили some_rev_list С "здание" some_building:

Foo Bar> some_rev_list 
RevisionList {theList = [1,4,2,5], revision = 2}

здесь уже есть хорошие ответы, поэтому этот ответ предназначен только как дополнение к прекрасным ответам от чи и Виллем Ван Онсем.

в основных объектно-ориентированных языках, таких как Java и C#, это не то, что объект по умолчанию неинициализированное; скорее, объект по умолчанию обычно инициализируется значениями по умолчанию для их типов, и просто случается, что для ссылочных типов значение по умолчанию равно null ссылка.

Haskell не имеет нулевых ссылок, поэтому записи не могут быть инициализированы с помощью нулей. Самым прямым переводом объектов будут записи, в которых каждый отдельный элемент является Maybe. Это не особенно полезно, однако, но это подчеркивает, как трудно защитить инварианты в ООП.

шаблон Builder не решает эту проблему вообще. Любой конструктор должен начинаться с начального объекта Builder, и этот объект должен иметь значений по умолчанию.

для более подробной информации и множества примеров я написал серия статей об этом. Серия статей специально посвящена шаблону построителя тестовых данных, но вы должны видеть, как он обобщается на шаблон Fluent Builder в целом.