Идентификаторы из State Monad в Haskell [дубликаты]

Возможный дубликат:
Создание уникальных меток в Haskell

У меня есть тип данных Person и некоторые входные данные, из которых я создам Persons.

Я бы хотел, чтобы у каждого человека был свой идентификатор (скажем, целые числа [0..]). Я мог бы сделать это с помощью рекурсии, но поскольку я делаю это на Haskell, я хотел бы понять монады. Полагаю, State Monad лучше всего подходит для этой работы?

Дело в том, что я не очень понимаю многие вещи: когда я нахожусь внутри монады (какие функции могут использовать внутреннюю часть), как мне их соединить вместе, как мне заставить функцию «галочка» продвигаться вперед и т. д. .

Итак, я в настоящее время застрял с этим: функция тика, вероятно, работает, но я не уверен, как ее использовать; и как последовательно получить его значение для построения Persons.

import Control.Monad.State

data Person = Person {
  id   :: Int,
  name :: String
} deriving Show

type MyState = Int
startState = 0

tick :: State MyState Int
tick = do
  n <- get
  put (n+1)
  return n

names = ["Adam","Barney","Charlie"]

-- ??? -> persons = [Person 0 "Adam", Person 1 "Barney", Person 2 "Charlie"]

main = do
  print $ evalState tick startState
  -- ???

РЕДАКТИРОВАТЬ: будет ли это как-то проще с Data.Unique или Data.Unique.Id? Как его использовать в моей ситуации?


person Martin Janiczek    schedule 17.10.2012    source источник
comment
Вам следует рассмотреть возможность более точного сопоставления вашего типа данных с вашей проблемой, например, с картой.   -  person Sarah    schedule 17.10.2012
comment
Вы можете изучить этот недавний вопрос о маркировке узлов дерева .   -  person Luis Casillas    schedule 18.10.2012


Ответы (4)


Ну, я думаю, что лучший способ объяснить это просто написать код.

Прежде всего, вы хотели бы также скрыть внутреннюю работу монады, в которой вы сейчас работаете. Мы сделаем это с помощью псевдонима типа, но есть и более мощные способы, см. эту главу из Хаскель реального мира.

type PersonManagement = State Int

Причина этого в том, что вы добавите больше вещей в PersonManagement позже, и хорошей практикой является использование абстракции черного ящика.

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

generatePersonId :: PersonManagement Int
generatePersonId = do
    n <- get
    put (n+1)
    return n

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

createPerson :: String -> PersonManagement Person
createPerson name = do
    id <- generatePersonId
    return $ Person id name

К настоящему времени вы, вероятно, поняли, что PersonManagement — это тип вычислений или процесс, который инкапсулирует логику для работы с Persons, а PersonManagement Person — это вычисление, из которого мы получаем объект person. Это очень мило, но как нам на самом деле получить людей, которых мы только что создали, и что-то с ними сделать, например, распечатать их данные на консоли. Что ж, нам нужен метод «запустить», который запускает наш процесс и выдает нам результат.

runPersonManagement :: PersonManagement a -> a
runPersonManagement m = evalState m startState

runPersonManagement запускает монаду и получает окончательный результат, выполняя все побочные эффекты в фоновом режиме (в вашем случае отмечая состояние Int). При этом используется evalState из монады состояния, и он также должен находиться в модуле, описанном выше, поскольку он знает о внутренней работе монады. Я предположил, что вы всегда хотите начинать идентификатор человека с фиксированного значения, определяемого startState.

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

work :: PersonManagement (Person, Person)
work = do
    john <- createPerson "John"
    steve <- createPerson "Steve"
    return (john, steve)

main = do
    let (john, steve) = runPersonManagement work
    putStrLn $ show john
    putStrLn $ show steve

Вывод:

Person {id = 0, name = "John"}
Person {id = 1, name = "Steve"}

Поскольку PersonManagement является полноценной монадой, вы также можете использовать универсальные функции из Control.Monad например. Допустим, вы хотите создать список лиц из списка имен. Ну, вот только функцию карты подняли в область монад - она ​​называется mapM.

createFromNames :: [String] -> PersonManagement [Person]
createFromNames names = mapM createPerson names

Использование:

runPersonManagement $ createFromNames ["Alice", "Bob", "Mike"] =>
    [
        Person {id = 0, name = "Alice"},
        Person {id = 1, name = "Bob"},
        Person {id = 2, name = "Mike"}
    ]

И примеры можно продолжать.

Чтобы ответить на один из ваших вопросов - вы работаете в монаде PersonManagement только тогда, когда вам нужны услуги, предоставляемые этой монадой - в этом случае функция generatePersonId или вам нужны функции, которые, в свою очередь, требуют примитивов монады, таких как work, которая нуждается в функции createPerson, которая в свою очередь, должен выполняться внутри монады PersonManagement, потому что ей нужен самоувеличивающийся счетчик. Если у вас есть, например, функция, которая проверяет, имеют ли два человека одинаковые данные, вам не нужно работать внутри монады PersonManagement, и это должна быть обычная, чистая функция типа Person -> Person -> Bool.

Чтобы действительно понять, как работать с монадами, вам просто нужно пройти через множество примеров. Real World Haskell — отличное начало, как и Изучите Haskell.

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

Кроме того, этот статья П. Вадлера содержит очень хорошие примеры и, конечно же, есть много других ресурсов, которые готовы к открытию.

person Marius Danila    schedule 17.10.2012
comment
Это потрясающий ответ. Спасибо. Я думаю, что теперь я получаю монады немного больше :) Абстракция также помогает и делает ее более понятной. - person Martin Janiczek; 17.10.2012
comment
Что делать, если вы хотите постоянно менять идентификаторы во время выполнения программы? Я чувствую, что мы застряли бы в монаде State. - person MFlamer; 24.01.2013

Монады в синтаксисе do во многом работают «как и следовало ожидать», рассматривая все это так, как если бы это был императивный язык.

Итак, что мы хотим здесь сделать с процедурной точки зрения? Перебрать заданные имена, верно? Как насчет

forM names

с forM из Control.Monad. Это очень похоже на цикл for, как вы его знаете. Хорошо, сначала нам нужно привязать каждое имя к переменной

forM names $ \thisName -> do

Что мы хотели бы сделать? Нам нужен идентификатор, tick сгенерирует его для нас

   newId <- tick

и объедините его с именем человека. И это все!

   return $ Person newId thisName

тогда все выглядит так:

(persons, lastId) = (`runState` startState) $ do
   forM names $ \thisName -> do
      newId <- tick
      return $ Person newId thisName

который работает так, как ожидалось, или работал бы, если бы в Ideone был установлен пакет mtl...

person leftaroundabout    schedule 17.10.2012

Лучше сделать с mapAccumL лайком

getPersons = snd . mapAccumL f 0
    where
        f n name = (n+1,Person n name)

В любом случае, я изменил вашу программу, чтобы она работала с монадой состояния

import Control.Monad.State

data Person = Person {
  id   :: Int,
  name :: String
} deriving Show

type MyState = Int
startState = 0

tick :: State MyState Int
tick = do
  n <- get
  put (n+1)
  return n

getPerson :: String -> State MyState Person
getPerson ps = do
  n <- tick
  return (Person n ps)


names = ["Adam","Barney","Charlie"]

getPersonsExample :: State MyState [Person]
getPersonsExample = do
    a <- getPerson "Adam"
    b <- getPerson "Barney"
    c <- getPerson "Charlie"
    return ([a,b,c])

main1 = do
  print $ evalState (sequence $ map getPerson names) startState

main2 = do
  print $ evalState getPersonsExample startState
person Satvik    schedule 17.10.2012
comment
Конечно, fold лучше в этом примере, где у нас есть просто список имен, но для реального применения решение State определенно намного удобнее. Но ваше решение на самом деле не использует монаду состояния, оно просто продолжает обертывать и разворачивать свой новый тип! - person leftaroundabout; 17.10.2012
comment
@leftaroundabout См. измененное решение - person Satvik; 17.10.2012
comment
Спасибо! То есть, если я правильно понимаю, если я в функции типа... -> State MyState..., то я могу использовать все остальные функции этого типа? (например, внутри getPerson я могу использовать галочку?) - person Martin Janiczek; 17.10.2012
comment
@MartinJaniczek Да, поскольку State MyState — это монада, вы можете использовать любую функцию типа State MyState a. - person Satvik; 18.10.2012

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

Data.Unique и Data.Unique.Id могут показаться проще, но следует помнить о двух моментах:

  1. Ваш код будет привязан к монаде IO.
  2. В модулях Unique не указана область, в которой сгенерированные идентификаторы уникальны. Заботится ли ваша программа о том, может ли один и тот же идентификатор быть назначен двум разным людям в разных запусках программы? Ваша программа полагается на возможность «восстановления» назначений Person-to-ID из предыдущих выполнений?

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

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

import Control.Monad (mapM) -- Study the Control.Monad module carefully...

-- Your "tick" action can be made more generic by using `Enum` instead of numbers
postIncrement :: Enum s => State s s
postIncrement = do r <- get
                   put (succ r)
                   return r

 -- Action to make a labeled Person from a name.
 makePersonM :: String -> State Int Person
 makePersonM name = do label <- postIncrement
                       return $ Person label name

-- The glue you're missing is mapM
whatYouWant = evalState (mapM makePersonM names) 0
person Luis Casillas    schedule 17.10.2012