Haskell:如何使用单子(monad)上下文编写带有参数的单子(monad)可变参数函数

标签 haskell types monads typeclass variadic-functions

我正在尝试使用一元返回类型制作一个可变参数函数,其参数也需要一元上下文。 (我不确定如何描述第二点:例如 printf 可以返回 IO () 但不同之处在于,无论它最终是 IO () 还是 String ,它的参数都被视为相同。)

基本上,我有一个数据构造函数,比如说,两个 Char参数。我想提供两个指针样式ID Char而是参数,可以从封闭的 State 自动解码monad 通过类型类实例。所以,不要做 get >>= \s -> foo1adic (Constructor (idGet s id1) (idGet s id2)) , 我想做 fooVariadic Constructor id1 id2 .

以下是我到目前为止所得到的,Literate Haskell 风格,以防有人想要复制它并弄乱它。

一、基本环境:

> {-# LANGUAGE FlexibleContexts #-}
> {-# LANGUAGE FlexibleInstances #-}
> {-# LANGUAGE MultiParamTypeClasses #-}

> import Control.Monad.Trans.State

> data Foo = Foo0
>          | Foo1 Char
>          | Foo2 Bool Char
>          | Foo3 Char Bool Char
>    deriving Show

> type Env = (String,[Bool])
> newtype ID a = ID {unID :: Int}
>    deriving Show

> class InEnv a where envGet :: Env -> ID a -> a
> instance InEnv Char where envGet (s,_) i = s !! unID i
> instance InEnv Bool where envGet (_,b) i = b !! unID i

为方便起见,一些测试数据:

> cid :: ID Char
> cid = ID 1
> bid :: ID Bool
> bid = ID 2
> env :: Env
> env = ("xy", map (==1) [0,0,1])

我有这个非单子(monad)版本,它只是将环境作为第一个参数。这很好用,但这不是我所追求的。例子:

$ mkFoo env Foo0 :: Foo
Foo0
$ mkFoo env Foo3 cid bid cid :: Foo
Foo3 'y' True 'y'

(我可以使用函数依赖或类型族来摆脱对 :: Foo 类型注释的需求。现在我对此并不在意,因为无论如何这不是我感兴趣的。)

> mkFoo :: VarC a b => Env -> a -> b
> mkFoo = variadic
>
> class VarC r1 r2 where
>    variadic :: Env -> r1 -> r2
>
> -- Take the partially applied constructor, turn it into one that takes an ID
> -- by using the given state.
> instance (InEnv a, VarC r1 r2) => VarC (a -> r1) (ID a -> r2) where
>    variadic e f = \aid -> variadic e (f (envGet e aid))
>
> instance VarC Foo Foo where
>    variadic _ = id

现在,我想要一个在以下 monad 中运行的可变参数函数。

> type MyState = State Env

基本上,我不知道我应该如何进行。我尝试过以不同的方式表达类型类( variadicM :: r1 -> r2variadicM :: r1 -> MyState r2 ),但我没有成功编写实例。我也尝试过调整上面的非单子(monad)解决方案,以便我以某种方式“结束” Env -> Foo然后我可以轻松地变成 MyState Foo ,但也没有运气。

以下是我迄今为止最好的尝试。

> mkFooM :: VarMC r1 r2 => r1 -> r2
> mkFooM = variadicM
>
> class VarMC r1 r2 where
>    variadicM :: r1 -> r2
>
> -- I don't like this instance because it requires doing a "get" at each
> -- stage. I'd like to do it only once, at the start of the whole computation
> -- chain (ideally in mkFooM), but I don't know how to tie it all together.
> instance (InEnv a, VarMC r1 r2) => VarMC (a -> r1) (ID a -> MyState r2) where
>    variadicM f = \aid -> get >>= \e -> return$ variadicM (f (envGet e aid))
>
> instance VarMC Foo Foo where
>    variadicM = id
>
> instance VarMC Foo (MyState Foo) where
>    variadicM = return

它适用于 Foo0 和 Foo1,但不限于此:

$ flip evalState env (variadicM Foo1 cid :: MyState Foo)
Foo1 'y'
$ flip evalState env (variadicM Foo2 cid bid :: MyState Foo)

No instance for (VarMC (Bool -> Char -> Foo)
                       (ID Bool -> ID Char -> MyState Foo))

(在这里我想摆脱对注释的需求,但是这个公式需要两个 Foo 的实例这一事实使这成为问题。)

我理解投诉:我只有一个来自 Bool -> Char -> Foo 的实例至ID Bool -> MyState (ID Char -> Foo) .但我做不到
它想要的实例,因为我需要 MyState在某个地方,这样我就可以
ID Bool变成 Bool .

我不知道我是完全偏离轨道还是什么。我知道我可以通过不同的方式解决我的基本问题(我不想用 idGet s 等价物到处污染我的代码),例如创建 liftA/liftM - 用于不同数量 ID 参数的样式函数,类型如 (a -> b -> ... -> z -> ret) -> ID a -> ID b -> ... -> ID z -> MyState ret ,但我花了太多时间思考这个问题。 :-) 我想知道这个可变参数解决方案应该是什么样子。

最佳答案

警告

最好不要将可变参数函数用于此类工作。您只有有限数量的构造函数,因此智能构造函数似乎没什么大不了的。您需要的 ~10-20 行比可变参数解决方案更简单且更易于维护。此外,应用解决方案的工作量要少得多。

警告

monad/applicative 与可变参数函数相结合是问题所在。 “问题”是用于可变参数类的参数添加步骤。基本类看起来像

class Variadic f where 
    func :: f 
    -- possibly with extra stuff

您可以通过具有表单的实例使其可变
instance Variadic BaseType where ...
instance Variadic f =>  Variadic (arg -> f) where ...

当您开始使用单子(monad)时,这会中断。在类定义中添加 monad 将防止参数扩展(对于某些 monad M,您将获得::M (arg -> f))。将它添加到基本情况将阻止在扩展中使用单子(monad),因为(据我所知)不可能将单子(monad)约束添加到扩展实例中。有关复杂解决方案的提示,请参阅 P.S.。

使用导致(Env -> Foo)的函数的解决方向更有希望。以下代码仍然需要 :: Foo类型约束并为简洁起见使用 Env/ID 的简化版本。
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses, TypeFamilies #-}

module Test where

data Env = Env
data ID a = ID
data Foo
    = Foo0
    | Foo1 Char
    | Foo2 Char Bool
    | Foo3 Char Bool Char
    deriving (Eq, Ord, Show)

class InEnv a where
    resolve :: Env -> ID a -> a
instance InEnv Char where
    resolve _ _ = 'a'
instance InEnv Bool where
    resolve _ _ = True

类型族扩展用于使匹配更严格/更好。现在是可变参数函数类。
class MApp f r where
    app :: Env -> f -> r

instance MApp Foo Foo where
    app _ = id
instance (MApp r' r, InEnv a, a ~ b) => MApp (a -> r') (ID b -> r) where
    app env f i = app env . f $ resolve env i
    -- using a ~ b makes this instance to match more easily and
    -- then forces a and b to be the same. This prevents ambiguous
    -- ID instances when not specifying there type. When using type
    -- signatures on all the ID's you can use
    -- (MApp r' r, InEnv a) => MApp (a -> r') (ID a -> r)
    -- as constraint.

环境Env是显式传递的,本质上是 Reader monad 被解包,防止了 monad 和可变参数函数之间的问题(对于 State monad,resolve 函数应该返回一个新环境)。使用 app Env Foo1 ID :: Foo 进行测试结果是预期的Foo1 'a' .

附言
你可以让一元可变函数工作(在某种程度上),但它需要以一些非常奇怪的方式弯曲你的函数(和头脑)。我让这些事情起作用的方法是将所有可变参数“折叠”到一个异构列表中。然后可以一元地完成展开。尽管我已经做过一些类似的事情,但我强烈建议您不要在实际(使用过的)代码中使用这些东西,因为它很快就会变得难以理解和无法维护(更不用说您会得到的类型错误)。

关于Haskell:如何使用单子(monad)上下文编写带有参数的单子(monad)可变参数函数,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/12175912/

相关文章:

haskell - 在 VS Code 中调试 haskell

haskell - 为什么我的数据类型需要一个 Monoid 实例才能使用这个镜头?

scala - 如何在 Scala 中为特定 Map 类型创建类型别名

haskell - 减少 Haskell 表达式

haskell - 绑定(bind)变量时Haskell中的无限循环

list - 列表推导式是 Haskell 的主要部分吗?

types - 如何在Clojure中构建健壮的数据API?

class - 用户自定义列表实例

haskell - 将更高种类的类型(单子(monad)!)嵌入到无类型的 lambda 演算中

haskell - 如何有条件地绑定(bind)在 do block 中?