【问题标题】:IDs from State Monad in Haskell [duplicate]来自 Haskell 中 State Monad 的 ID [重复]
【发布时间】:2012-10-08 03:24:55
【问题描述】:

可能重复:
Creating unique labels in 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 会更容易吗?在我的情况下如何使用它?

【问题讨论】:

标签: haskell monads uniqueidentifier state-monad


【解决方案1】:

嗯,我认为最好的解释方式就是写一些代码。

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

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

runPersonManagement 运行 monad 并获得最终结果,同时在后台执行所有副作用(在您的情况下,勾选 Int 状态)。这使用来自状态 monad 的evalState,它也应该驻留在上述模块中,因为它知道 monad 的内部工作原理。我假设您总是希望从一个固定值开始人员 ID,由 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 是一个成熟的 monad,您还可以使用来自 Control.Monad 的泛型函​​数,例如。假设您想从姓名列表中创建人员列表。好吧,这只是在 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 函数或您需要函数,而这些函数又需要 monad 的原语,如 work需要 createPerson 函数,该函数又需要在 PersonManagement monad 中运行,因为它需要自增计数器。例如,如果你有一个检查两个人是否有相同数据的函数,你就不需要在 PersonManagement monad 中工作,它应该是一个普通的、纯粹的 Person -&gt; Person -&gt; Bool 类型的函数。

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

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

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

【讨论】:

  • 这是一个很棒的答案。谢谢你。我想我现在对 monad 有了更多了解 :) 抽象也有助于并使其更清晰。
  • 如果您想在整个程序执行过程中不断更改 ID 怎么办?我觉得我们会被“困”在 State monad 中。
【解决方案2】:

do 语法中的 Monad 在很多方面都非常“如您所愿”,将整个事物视为一种命令式语言。

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

forM names

forM 来自Control.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

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

【讨论】:

    【解决方案3】:

    最好用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
    

    【讨论】:

    • 当然fold 在这个例子中更好,我们只是有一个名称列表,但对于这个的实际应用,State 解决方案肯定更容易处理。但是您的解决方案实际上并没有使用 state monad,它只是不断地包装和展开它的新类型!
    • @leftaroundabout 查看修改后的解决方案
    • 谢谢!因此,如果我理解正确,如果我处于...类型的函数中 -> State MyState ...,那么我可以使用该类型的所有其他函数吗? (例如,在 getPerson 中我可以使用 tick 吗?)
    • @MartinJaniczek 是的,因为State MyState 是Monad,你可以使用State MyState a 类型的任何函数。
    【解决方案4】:

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

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

    1. 您的代码将绑定到 IO monad。
    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
    

    【讨论】:

      猜你喜欢
      • 2013-05-16
      • 1970-01-01
      • 2018-08-27
      • 2014-09-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-06-30
      相关资源
      最近更新 更多