首页 文章

来自Haskell的State Monad的ID [复制]

提问于
浏览
0

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

我有一个数据类型Person和一些输入数据,我将从中创建人员 .

我想让每个人都有自己的ID(假设整数[0 ..]) . 我可以通过递归来做到这一点,但是因为我在Haskell中这样做,所以我想了解monad . 我想,State Monad可能是这项工作的最佳人选 .

问题是,我并不是很了解很多东西:当我在monad中时(什么功能可以使用内部),我如何将它们连接在一起,如何使'tick'功能提前等等 . .

所以我现在坚持这个:滴答功能可能有效,但我不知道如何使用它;以及如何相继获得人才建设的 Value .

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 回答

  • 0

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

    首先,你想隐藏你当前工作的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是一种计算类型,或者是一个封装用于处理Persons的逻辑的进程,而 PersonManagement Person 是我们从中获取person对象的计算 . 这是非常好的,但我们如何实际获得我们刚创建的人并使用它们做一些事情,比如在控制台上打印他们的数据 . 好吧,我们需要一个"run"方法,它运行我们的过程并给我们结果 .

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

    runPersonManagement运行monad并在后台执行所有副作用时获得最终结果(在您的情况下,勾选Int状态) . 这使用来自状态monad的evalState,它也应该驻留在上面描述的模块中,因为它知道monad的内部工作方式 . 我假设你总是希望从一个由startState标识的固定值启动person 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中的泛型函数 . 让's say you want to create a list of persons from a list of names. Well, that'只是在monad域中解除的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函数或者你需要函数,而这些函数又需要像 work 这样需要 createPerson 函数的monad原语 . 反过来需要在PersonManagement monad中运行,因为它需要自增量计数器 . 例如,如果你有一个检查两个人是否有相同数据的函数,你就不需要在PersonManagement monad中工作,它应该是 Person -> Person -> Bool 类型的普通纯函数 .

    要真正了解如何使用monad,您只需要经过大量示例 . Real World Haskell是一个很好的开始,所以Learn you a Haskell .

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

    此外,P. Wadler的这个paper提供了一些非常好的例子,当然,还有更多的资源可供发现 .

  • 6

    do 语法中的Monads在很多方面都很有效_11343426_,将整个事情视为命令式语言 .

    那么从程序上讲,我们想做什么呢?迭代给定的名字,对吗?怎么样

    forM names
    

    来自 Control.MonadforM . 那非常像 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
    

    哪个works as expected,或者如果Ideone安装了mtl包...

  • 1

    更喜欢 mapAccumL 之类的

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

    无论如何我修改了你的程序,使其与状态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
    
  • 1

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

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

    • 您的代码将绑定到 IO monad .

    • Unique 模块没有明确说明生成的ID唯一的范围 . 您的程序是否关心是否可以在程序的不同运行中将相同的ID分配给两个不同的人员?您的程序是否依赖于能够"reinstate"之前执行的人员到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
    

相关问题