【问题标题】:How do I combine Result and State in Elm?如何在 Elm 中结合结果和状态?
【发布时间】:2017-08-21 06:52:00
【问题描述】:

我正在为 Elm 缺乏单子而苦苦挣扎。为 Elm (http://package.elm-lang.org/packages/folkertdev/elm-state/latest/State) 实现 state monad 的库对我帮助很大。

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

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

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

如果我将 Result 放在 State 中的返回值中,也许它会起作用,但如果外部 Result 是Err,那会生成一个虚拟状态。

我认为正确的想法是制作既是结果又是状态的东西。熟悉 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))
  )

【问题讨论】:

  • 当你混合相同类型的变压器时,Monad 变压器有点痛苦。我通常使用 monad 类型类,但是当类型出现多次时它们会失败。一个解决方案是新输入您需要的特定上下文。
  • 我真的不需要多次使用同一个单子。我包含的代码 sn-p 是generateConstraints 的一种情况,它返回Result String (State Int (Type, List Constraint))。递归可以正常工作,但是对于 let 绑定的约束生成有一些不同之处,它会弄乱 monad。
  • 我仍然不知道如何结合有状态和可能是错误的,但我给出的特定示例奇迹般地自行解决了,因为我能够将所有异常结果移出。

标签: haskell monads elm monad-transformers


【解决方案1】:

熟悉 Haskell monad 转换器的人能否解释一下他们如何解决此类问题或提出替代解决方案?

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

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

一个相当明显的观察是它不是一个单子转换器。2这就是一个转换器的样子:

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

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

核心思想是State 是结果产生的一部分,无论Either 导致“成功”还是“失败”,而在您的情况下产生“失败”意味着状态操作永远不会感动。我需要更加努力地思考这在实践中意味着什么,但直观地说,在使用典型的命令式代码时,你会想到转换器方法。

现在,当你使用这样的转换器时,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 一种可能的方法。还有其他递归方案/免费可能是未来几年值得关注的方案。事实证明,效果堆叠的固有顺序比最初看起来更成问题。

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

【讨论】:

  • 你有点太慢了;我已经自己解决了这个问题。在我的情况下,EitherT State 会起作用吗?至少我写它的方式,它只适用于相反的方式。
  • join 是免费提供的,因为它与>>= 几乎相同,对吧?另外,在我的 andThen 中有非常丑陋的Err e -> State.state <| Err e。我认为至少在 Elm 中必须这样做,因为 Err e -> x 不进行类型检查。
  • @Joonazan 我看到了你的评论,但我想我还是会回答。我认为它适用于您的情况;这些变压器的顺序无关紧要。是的,join 就是join x = x >>= id。你的实现的“丑陋”主要是因为你必须同时解开两个层,而转换器可以将其拆分并允许稍后组合。
  • 您能详细说明一下吗?我的解释是,如果我有一个 Result State,那么 Result 的值就不能依赖于 State。然而它必须这样做,因为要实现 let 的类型推断,我必须在推断 let 的主体之前使用声明值的类型扩展环境。
  • @Joonazan 当你有一个ResultT State 或StateT Result,你可以有效地同时写both。核心思想是你只产生一次值,唯一的区别是你如何访问上下文函数(lift,liftIO,mtl typeclasses...)
【解决方案2】:

我最终写了一个兼具两者的 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

【讨论】:

    猜你喜欢
    • 2010-11-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-05-05
    • 1970-01-01
    • 1970-01-01
    • 2019-01-29
    • 1970-01-01
    相关资源
    最近更新 更多