4

私は、次のステートフルな命令型コードを純粋な関数表現に変換する最もエレガントな方法を見つけようとしています (Haskell では、Monad 実装が提供する抽象化を使用することが望ましいです)。ただし、トランスフォーマーなどを使用して異なるモナドを組み合わせるのはまだ得意ではありません。私には、そのようなタスクに対する他の人の見方を分析することは、自分でそれを行う方法を学ぶときに最も役立つように思えます. 命令コード:

while (true) {
  while (x = get()) { // Think of this as returning Maybe something
    put1(x) // may exit and present some failure representation
  }
  put2() // may exit and present some success representation
}

get戻るときはNothingを続行するために実行が必要です。戻るときはput2に渡して、失敗した場合にのみ短絡するか、それ以外の場合はループする必要があります。基本的に、全体を終了するか、次のステートメントに移動して、基になる状態を何らかの形で変更することができます。成功して呼び出してループするか、失敗して続行するかのいずれかです。getJust xxput1put1put1put2getput1put2

私のアイデアは次のようなものでした:

forever $ do
  forever (get >>= put1)
  put2

そして、私がそのようなものを探していた理由は、何も返さないか終了する(get >>= put1)たびに単純に短絡する可能性があるためです。同様に、外側のループを終了します。ただし、必要なものと混合する方法、および/またはこれを達成する方法がわかりません。getput1put2StateMaybeEither

トランスフォーマーを使用しStateて他のモナドを結合する必要があるため、コードはおそらくそれほど簡潔ではないと思います。しかし、それもそれほど悪くはないと思います。

翻訳をエレガントに実現する方法についての提案は大歓迎です。これは、、を使用した明示的な制御フローを回避する点で「さまざまなタイプのブレークを伴うステートフル ループ」とは異なり、 、、またはその他の便利なセマンティクスの使用を奨励しようとします。また、コードを機能的なものに変換する簡単な方法は常にありますが、それはエレガントとは言えません。ifwhenwhileMaybeEither>>=

4

4 に答える 4

5

EitherTまたはを探していExceptTます。Transformer スタックに戻る 2 つの方法を追加します。計算は、 または のいずれreturn athrowError eです。エラーとリターンには 2 つの違いがあります。エラーは で保持され、Leftで返されますRight>>=エラーが発生すると、短絡します。

newtype EitherT e m a = EitherT { runEitherT :: m (Either e a) }

return :: a -> EitherT e m a
return a = EitherT $ return (Right a)

throwError :: e -> EitherT e m a
throwError e = EitherT $ return (Left a)

left = throwErrorとの名前も使用しますright = return

Don't continueのエラーLeft。ループからの終了を表すために使用します。このタイプEitherT r m ()を使用して、中断の結果で停止するか、 でLeft r継続するループを表しますRight ()foreverこれは、 をアンラップして、戻り値EitherTの周りの を取り除くことを除いて、ほぼ正確に です。Left

import Control.Monad
import Control.Monad.Trans.Either

untilLeft :: Monad m => EitherT r m () -> m r
untilLeft = liftM (either id id) . runEitherT . forever   

例を具体化した後、これらのループの使用方法に戻ります。

ほとんどすべてのロジックが消えるのを見たいので、EitherT他のすべてにも使用します。データを取得する計算は、Doneまたはデータを返します。

import Control.Monad.Trans.Class
import Control.Monad.Trans.State

data Done = Done       deriving Show

-- Gets numbers for a while.
get1 :: EitherT Done (State Int) Int
get1 = do
    x <- lift get
    lift . put $ x + 1
    if x `mod` 3 == 0
    then left Done
    else right x

データを配置する最初の計算は、Failureまたは戻り値のいずれかです。

data Failure = Failure deriving Show

put1 :: Int -> EitherT Failure (State Int) ()
put1 x = if x `mod` 16 == 0
         then left Failure
         else right ()

データを配置する 2 番目の計算は、Successまたは戻り値のいずれかです。

data Success = Success deriving Show

put2 :: EitherT Success (State Int) ()
put2 = do 
        x <- lift get
        if x `mod` 25 == 0
        then left Success
        else right ()

あなたの例では、異なる方法で例外的に停止する 2 つ以上の計算を組み合わせる必要があります。これを 2 つEitherTのネストされた で表します。

EitherT o (EitherT i m) r

外側EitherTは、現在操作中のものです。すべての†</sup>の周りに追加のレイヤーを追加することで、 anEitherT o m aを an に変換できます。EitherT o (EitherT i m) aEitherTm

over :: (MonadTrans t, Monad m) => EitherT e m a -> EitherT e (t m) a
over = mapEitherT lift

内側のEitherTレイヤーは、トランスフォーマー スタック内の他の基礎となるモナドと同じように扱われます。私たちはliftするEitherT i m aことができますEitherT o (EitherT i m) a

これで、成功または失敗する全体的な計算を構築できます。現在のループを破る計算が実行されoverます。外側のループを壊す計算はlifted です。

example :: EitherT Failure (State Int) Success
example =
    untilLeft $ do
        lift . untilLeft $ over get1 >>= lift . put1
        over put2

全体Failurelift、最も内側のループに 2 回追加されます。この例は、いくつかの異なる結果を見るのに十分興味深いものです。

main = print . map (runState $ runEitherT example) $ [1..30]

†</sup>EitherTMFunctorインスタンスがあれば、overただhoist lift. ちなみに、私がEitherTover をExceptT主に使用しているのは、名前の負荷が少ないためです。どちらMFunctorが最初にインスタンスを提供しても、私にとっては、最終的にどちらかのモナド変換子として勝ちます。

于 2015-09-04T16:26:08.997 に答える
1

本当にエレガントではないもののエレガントな方法を求めているので、あなたの質問は少しトリッキーです。そのタイプのループを記述するためのControl.Monad.Loopsがあります。whileJust'おそらく、同等のものが必要になるでしょう。while通常、そのようなループを記述する必要はなく、単純な古い再帰が通常は最も簡単です。

このタイプのコードが必要な場合の例を見つけようとしたところ、次の例が見つかりました。ユーザーが入力した文字列のリストのリストを作成したいと思います。各行は、リスト内のエントリに対応しています。空行は新しいリストを開始し、2 つの空行はループを停止します。

a
b
c

d
e

f

あげる

[ ["a", "b", "c"
, ["d", "e"]
, ["f"]
]

私はおそらくhaskellで次のことをするでしょう

readMat :: IO [[String]]
readMat = reverse `fmap` go [[]]
    where go sss = do
                s <- getLine
                case s of
                    "" -> case sss of
                        []:sss' -> return sss' # the end
                        _ -> go ([]:sss)       # starts a new line
                    _ -> let (ss:ss') = sss
                          in go ((ss ++ [s]):ss')

ただの再帰。

于 2015-09-04T16:25:58.777 に答える
1

ただし、トランスフォーマーなどを使用して異なるモナドを組み合わせるのはまだ得意ではありません。

異なるモナドをコンビネータと組み合わせる必要は実際にはありません。State モナドに Maybe モナドを明示的に埋め込むだけで済みます。これが完了すると、スニペットの変換は簡単で、ループを相互に再帰的な関数に置き換えます。相互関係は分岐条件を実装します。

OCaml と、State モナドが Lemonade_Success と呼ばれる輝くモナド ライブラリ Lemonadeを使用して、このソリューションを書きましょう。

したがって、put1put2によって返されるエラーを表す型は、診断メッセージを表す文字列であると想定し、String 型で Success モナドをインスタンス化します。

Success =
  Lemonade_Success.Make(String)

現在、Success モジュールは、診断で失敗する可能性のあるモナド計算を表しています。Success の完全な署名については、以下を参照してください。上記のスニペットの翻訳を、データによってパラメーター化されたファンクターとして記述しますが、もちろん、これをショートカットして実装定義を直接使用することもできます。問題のデータは、署名 P を持つモジュール パラメータによって記述されます。

module type P =
sig
    type t
    val get : unit -> t option
    val put1 : t -> unit Success.t
    val put2 : unit -> unit Success.t
end

上記のスニペットの可能な実装は次のようになります

module M(Parameter:P) =
struct
    open Success.Infix

    let success_get () =
      match Parameter.get () with
        | Some(x) -> Success.return x
        | None -> Success.throw "Parameter.get"

    let rec innerloop () =
      Success.catch
        (success_get () >>= Parameter.put1 >>= innerloop)
        (Parameter.put2 >=> outerloop)
    and outerloop () =
      innerloop () >>= outerloop
end

関数 get_success は、Maybe モナドを Success モナドにマップし、アドホックなエラーの説明を提供します。これは、抽象的なモナドコンビネータのみを使用してこの変換を行うことができないというアドホックエラーの説明が必要なためです。または、これをもっとペダン的に表現すると、Maybe から State への標準的なマッピングはありません。これらのマッピングはパラメータ化されているためです。エラーの説明によって。

success_get 関数を記述したら、相互再帰関数と、エラー条件の処理に使用される Success.catch 関数を使用して、説明した分岐条件を簡単に変換できます。

Haskell での実装は演習として残します。:)


Success モジュールの完全な署名は次のとおりです。

  module Success :
  sig
    type error = String.t
    type 'a outcome =
      | Success of 'a
      | Error of error
    type 'a t
    val bind : 'a t -> ('a -> 'b t) -> 'b t
    val return : 'a -> 'a t
    val apply : ('a -> 'b) t -> 'a t -> 'b t
    val join : 'a t t -> 'a t
    val map : ('a -> 'b) -> 'a t -> 'b t
    val bind2 : 'a t -> 'b t -> ('a -> 'b -> 'c t) -> 'c t
    val bind3 : 'a t -> 'b t -> 'c t -> ('a -> 'b -> 'c -> 'd t) -> 'd t
    val bind4 :
      'a t -> 'b t -> 'c t -> 'd t -> ('a -> 'b -> 'c -> 'd -> 'e t) -> 'e t
    val map2 : ('a -> 'b -> 'c) -> 'a t -> 'b t -> 'c t
    val map3 : ('a -> 'b -> 'c -> 'd) -> 'a t -> 'b t -> 'c t -> 'd t
    val map4 :
      ('a -> 'b -> 'c -> 'd -> 'e) -> 'a t -> 'b t -> 'c t -> 'd t -> 'e t
    val dist : 'a t list -> 'a list t
    val ignore : 'a t -> unit t
    val filter : ('a -> bool t) -> 'a t list -> 'a list t
    val only_if : bool -> unit t -> unit t
    val unless : bool -> unit t -> unit t
    module Infix :
      sig
        val ( <*> ) : ('a -> 'b) t -> 'a t -> 'b t
        val ( <$> ) : ('a -> 'b) -> 'a t -> 'b t
        val ( <* ) : 'a t -> 'b t -> 'a t
        val ( >* ) : 'a t -> 'b t -> 'b t
        val ( >>= ) : 'a t -> ('a -> 'b t) -> 'b t
        val ( >> ) : 'a t -> (unit -> 'b t) -> 'b t
        val ( >=> ) : ('a -> 'b t) -> ('b -> 'c t) -> 'a -> 'c t
        val ( <=< ) : ('b -> 'c t) -> ('a -> 'b t) -> 'a -> 'c t
      end
    val throw : error -> 'a t
    val catch : 'a t -> (error -> 'a t) -> 'a t
    val run : 'a t -> 'a outcome
  end

簡潔にするために、いくつかの型注釈を削除Tし、署名から自然な変換を隠しました。

于 2015-09-04T14:55:51.253 に答える