0

可能重复:
在 Haskell 中创建唯一标签

我有一个数据类型 Person 和一些用于创建 Persons 的输入数据。

我想让每个人都有自己的 ID(比如说整数 [0..])。我可以通过递归来做到这一点,但由于我在 Haskell 中这样做,我想了解单子。我想,State Monad 可能是这项工作的最佳选择?

问题是,我真的不明白很多事情:我什么时候在 monad 里面(什么函数可以使用里面),我如何将它们组合在一起,我如何让“tick”函数前进,等等。 .

所以我目前对此感到困惑:tick 功能可能有效,但我不确定如何使用它;以及如何依次获取其用于构建 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 会更容易吗?在我的情况下如何使用它?

4

4 回答 4

7

好吧,我认为最好的解释方法就是编写一些代码。

首先,你也想隐藏你当前工作的 monad 的内部工作。我们将使用类型别名来做到这一点,但还有更强大的方法,请参阅Real World Haskell中的这一章。

type PersonManagement = State Int

这样做的原因是您稍后会向 PersonManagement 添加更多内容,以及使用黑盒抽象的良好实践。

连同 PersonManagement 的定义,您应该公开定义此 monad 的原始操作。在您的情况下,我们现在只有 tick 函数,它看起来几乎相同,但具有更清晰的签名和更具暗示性的名称。

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

现在,以上所有内容都应该位于一个单独的模块中。在此之上,我们可以定义更复杂的操作,比如创建一个新的 Person:

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

现在你可能已经意识到,PersonManagement 是一种计算类型,或者是一个封装了处理 Person 的逻辑的过程,并且PersonManagement Person是我们从中获取 person 对象的计算。这非常好,但是我们如何真正获取我们刚刚创建的人并对他们做一些事情,比如在控制台上打印他们的数据。好吧,我们需要一个“运行”方法,它运行我们的过程并给我们结果。

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

runPersonManagement 运行 monad 并在后台执行所有副作用时获得最终结果(在您的情况下,勾选 Int 状态)。这使用了state monad 中的evalState,它也应该驻留在上述模块中,因为它知道 monad 的内部工作原理。我假设,您总是希望从一个由 startState 标识的固定值开始人员 ID。

例如,如果我们想创建两个人并将他们打印到控制台,程序将类似于:

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 是一个成熟的 monad,您还可以使用Control.Monad中的通用函数,例如。假设您想从姓名列表中创建人员列表。好吧,这只是在 monads 领域中提升的 map 函数——它被称为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"}
    ]

例子可以继续。

要回答您的一个问题-仅当您需要该 monad 提供的服务时,您才在 PersonManagement monad 中工作-在这种情况下, generatePersonId 函数或您需要函数,而这些函数又需要 monad 的原语,例如work需要该createPerson函数的函数需要在 PersonManagement monad 内部运行,因为它需要自增计数器。例如,如果你有一个检查两个人是否有相同数据的函数,你就不需要在 PersonManagement monad 中工作,它应该是一个普通的纯函数 type Person -> Person -> Bool

要真正了解如何使用 monad,您只需要阅读大量示例即可。Real World Haskell是一个很好的开始,Learn you a Haskell也是如此。

您还应该查看一些使用 monad 的库,以了解它们是如何制作的以及人们如何使用它们。解析器就是一个很好的例子,而parsec是一个很好的起点。

此外,P. Wadler 的这篇论文提供了一些非常好的示例,当然,还有更多资源可供发现。

于 2012-10-17T19:55:09.610 回答
1

最好用mapAccumLlike

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

无论如何,我修改了您的程序以使其与 state monad

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
于 2012-10-17T19:36:07.520 回答
1

语法中的Monad 在do许多方面都非常“像你所期望的那样”工作,将整个事物视为一种命令式语言。

那么,从程序上讲,我们想在这里做什么?遍历给定的名称,对吗?怎么样

forM names

forMControl.Monad. _ 如您所知,这非常像一个for循环。好的,首先我们需要将每个名称绑定到一个变量

forM names $ \thisName -> do

我们想做什么我们需要一个 ID,tick将为我们生成它

   newId <- tick

并将其与人名结合起来。就是这样!

   return $ Person newId thisName

整个事情看起来像这样:

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

可以按预期工作,或者如果 Ideone 安装了 mtl 包...

于 2012-10-17T19:44:06.857 回答
0

这里真正的困难是定义和处理期望标识符唯一的范围。使用Statemonad 和一个Succ实例(如下面的示例)可以很容易地进行按摩,以保证在单个Statemonad 计算范围内的唯一性。稍加注意(在 a 之后捕获最终状态runState并确保将其用作 next 中的初始状态runState),您可以保证多次State计算的唯一性——但最好将两个计算组合成一个更大的计算。

Data.Unique并且Data.Unique.Id可能看起来更容易,但要记住两个问题:

  1. 您的代码将绑定到IOmonad。
  2. 这些Unique模块没有明确说明生成的 ID 的唯一范围。您的程序是否关心相同的 ID 是否可能在程序的不同运行中分配给两个不同的人?您的程序是否依赖于能够“恢复”之前执行的 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
于 2012-10-17T22:04:09.760 回答