如何在 Elm 中结合结果和状态?

How do I combine Result and State in Elm?

我正在为 Elm 缺少 monad 而苦恼。为 Elm (http://package.elm-lang.org/packages/folkertdev/elm-state/latest/State) 实现状态 monad 的库对我帮助很大。

问题是我现在 运行 遇到了交替嵌套 Result 和 State 类型的情况,而我只想拥有其中一个。

我尝试编写一个具有以下签名的函数,但这似乎是不可能的,因为内部结果只有在外部状态被评估后才知道。

join : Result a (State s (Result a (State s x))) -> Result a (State s x)

如果我将 Result 放入 return 值的 State 中,也许它会起作用,但如果外部 Result 是 Err.

,那会生成一个虚拟 State

我认为正确的想法是制作既是结果又是状态的东西。熟悉 Haskell monad 转换器的人可以解释一下他们如何解决此类问题或提出替代解决方案吗?

这是出现问题的一个地方的粗略版本:

  generateConstraints environment value
  |> Result.map (State.map (\(value, valueC) ->
    Result.map
      (State.map2 (\this (body, bodyC) ->
        ( this
        , valueC ++ bodyC ++ [(this, body)]
        ))
      freshTypevar)
      (generateConstraints (extend environment name value) body))
  )

Can someone who is familiar with Haskell monad transformers explain how they solve this kind of problem or suggest an alternative solution?

好吧,我至少可以试试。这就是你的类型看起来直接翻译成 Haskell:

type EffM a s x = Either a (State s x)

一个相当明显的观察是它不是 monad 转换器。2 转换器看起来像这样:

type TransM a s x = EitherT a (State s) x

如您所见,唯一的变化是 T 以及 x 在括号之外的事实。后一部分对于理解转换器方法至关重要。

核心思想是 State 是结果生成的一部分,无论 Either 是否导致 "success" 或 "fail",而在您的情况下producing "fail" 表示从未触及状态操作。我需要更深入地思考这在实践中意味着什么,但直觉上,转换器方法是您在使用典型的命令式代码时会想到的。

现在,当您使用这样的转换器时,join 实际上是免费提供的,因为它适合 Monad 界面。

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

type Eff e s a = EitherT e (State s) a

-- type the following in REPL

λ :t join
join :: Monad m => m (m a) -> m a

λ :t join :: Eff e s (Eff e s a) -> Eff e s a
join :: Eff e s (Eff e s a) -> Eff e s a
     :: Eff e s (Eff e s a) -> Eff e s a

-- the slightly cryptic output above means that it typechecks correctly

所以Haskell1就是这样解决的。现在,显然可以用这种方式编写专门的 EitherState 类型(尽管我个人将所有示例中的这两个翻转为 StateEither - 感觉更自然),反映了 join 对于各自的变压器就可以了。我不知道在 Elm 中是否可以专门编写 EitherT


1 一种可能的方法。还有其他的,递归 schemes/Free 可能是未来几年值得关注的。结果表明,效果堆叠的固有顺序比最初看起来更成问题。

2 它也不是 Monad,至少在 x 不能是 Monad 实例中的直接类型的意义上(因为 Either 在特殊情况下显然可以充当一个。

我最终写了一个兼具两者的 monad。我不得不牺牲在 State 被触及之前失败的能力,因为我需要能够在之后失败。

type alias Infer a = State Int (Result String a)

infer : a -> Infer a
infer x =
  State.state (Ok x)

map : (a -> value) -> Infer a -> Infer value
map f x =
  State.map (Result.map f) x

andThen : (a -> Infer b) -> Infer a -> Infer b
andThen f x =
  State.andThen
    (\r -> case r of
      Ok v -> f v
      Err e -> State.state <| Err e
    )
    x

andMap : Infer y -> Infer (y -> z) -> Infer z
andMap y =
  andThen (\g -> map g y)

map2 : (a -> b -> c) -> Infer a -> Infer b -> Infer c
map2 f x y =
  map f x
  |> andMap y

map3 : (a -> b -> c -> d) -> Infer a -> Infer b -> Infer c -> Infer d
map3 f a b c =
  map2 f a b
  |> andMap c

map4 : (a -> b -> c -> d -> e) -> Infer a -> Infer b -> Infer c -> Infer d -> Infer e
map4 f a b c d =
  map3 f a b c
  |> andMap d