0

重複の可能性:
Haskellで一意のラベルを作成する

データ型Personと、Personを作成するための入力データがあります。

各Personに独自のID(整数[0 ..]としましょう)を持たせたいのですが。これは再帰で行うことができますが、Haskellでこれを行っているので、モナドを理解したいと思います。州のモナドはおそらくこの仕事に最適だと思いますか?

問題は、私は多くのことを本当に理解していないということです:私がモナドの中にいるとき(どの関数が内部を使用できるか)、どのようにそれらを一緒に配管するか、どのように「ダニ」関数を前進させるかなど。 。

だから私は現在これに固執しています:tick関数はおそらく機能しますが、それを使用する方法がわかりません。そして、人の構築のためにその価値を連続的に得る方法。

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

ええと、説明する最善の方法は、コードを書くことだと思います。

まず第一に、現在作業しているモナドの内部の仕組みを隠したいと思うでしょう。これは型エイリアスで行いますが、もっと強力な方法があります。Real World Haskellのこの章を参照してください。

type PersonManagement = State Int

これは、後で PersonManagement にさらに何かを追加する場合に備えて、ブラック ボックスの抽象化を使用することをお勧めします。

PersonManagement の定義とともに、このモナドを定義するプリミティブ操作を公開する必要があります。あなたの場合、今のところ 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 オブジェクトを取得する計算であることに気付いたでしょう。それはとてもいいことですが、作成したばかりの人を実際に取得して、コンソールでデータを出力するなど、何かを行うにはどうすればよいでしょうか。プロセスを実行して結果を返す「run」メソッドが必要です。

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

runPersonManagement はモナドを実行し、バックグラウンドですべての副作用を実行しながら最終結果を取得します (この場合、Int 状態をチェックします)。これは状態モナドのevalStateを使用し、モナドの内部動作について知っているため、上記のモジュールにも存在する必要があります。startState で識別される固定値から個人 ID を常に開始する必要があると想定しました。

たとえば、2 人の人物を作成してコンソールに出力する場合、プログラムは次のようになります。

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の汎用関数を使用することもできます。名前のリストから人のリストを作成したいとしましょう。まあ、それはモナドのドメインで持ち上げられた 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"}
    ]

そして、例は続く可能性があります。

あなたの質問の 1 つに答えるために - PersonManagement モナドで作業するのは、そのモナドによって提供されるサービスが必要な場合のみです - この場合、generatePersonId 関数またはモナドのプリミティブworkを必要とするcreatePerson関数が必要です。自己インクリメントカウンターが必要なため、PersonManagement モナド内で実行する必要があります。たとえば、2 人の人物が同じデータを持っているかどうかをチェックする関数がある場合、PersonManagement モナド内で作業する必要はなく、通常の純粋な type の関数であるべきPerson -> Person -> Boolです。

モナドの扱い方を本当に理解するには、多くの例を見ていく必要があります。Real World Haskellは素晴らしいスタートであり、Learn you a Haskell も同様です。

また、モナドを使用するいくつかのライブラリを調べて、モナドがどのように作成され、人々がどのように使用するかを確認する必要があります。1 つの良い例はパーサーであり、parsecは開始するのに最適な場所です。

また、P. Wadler によるこの論文には、いくつかの非常に優れた例が示されています。もちろん、発見される準備ができているリソースは他にもたくさんあります。

于 2012-10-17T19:55:09.610 に答える
1

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

構文内のモナドはdo、多くの点で「期待どおり」に機能し、すべてを命令型言語であるかのように扱います。

では、手続き的に言えば、ここで何をしたいのでしょうか? 与えられた名前を繰り返しますよね?どうですか

forM names

forMからControl.Monad。_ forご存じのように、これはループによく似ています。まず、各名前を変数にバインドする必要があります

forM names $ \thisName -> do

をしたいですか?ID が必要です。IDtickを生成します

   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

ここでの本当の難しさは、識別子が一意であると予想される範囲を定義して処理することです。StateモナドとSuccインスタンス (以下の私の例のように) を使用すると、単一のStateモナド計算の範囲で一意性を保証するために簡単にマッサージできます。少し気をつければ ( a の後の最終状態を取得runStateし、次の の初期状態として確実に使用するようにしrunStateます)、複数の計算で一意性を保証できますがState、2 つの計算を 1 つの大きな計算にまとめた方がよいでしょう。

Data.Unique簡単にData.Unique.Id思えるかもしれませんが、次の 2 つの点に注意してください。

  1. IOコードはモナドに結び付けられます。
  2. Uniqueモジュールは、生成された ID が一意であるスコープについて明示的ではありません。あなたのプログラムは、プログラムの異なる実行で同じ ID が 2 人の異なる人物に割り当てられる可能性があるかどうかを気にしますか? あなたのプログラムは、以前の実行から 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 に答える