试图组成非组合:对接方案

引言


在Haskell中,习惯将效果用作函子,其对象是我们目前感兴趣的某些表达式。

当我们看到表达式可能是a的类型时,我们从a的实际存在中抽象出来,将所有注意力集中在a上 。 与List a相同的故事-a的复数值; 状态sa - a ,取决于某些当前状态; ea - a都可能返回一些错误e

在继续之前,本文将使用几个定义:

type (:=) ta = ta -- |   type (:.) tua = t (ua) -- |   type (~>) tu = forall a . ta -> ua -- |   

例如: 清单:。 也许:= a-这个表达式很容易想象,这是一个存在疑问的值列表。

此外,作为示例,我们将使用四种常见类型: ReaderStateEitherMaybe

成分和变压器


对一个表达式应用多个效果的最明显方法是将一个效果简单地嵌入另一个效果,这是函子的通常组成。 在合成中,效果不会互相影响(除非对它们使用可遍历的方法)。 为了将许多效果合并为一个,使用了变压器。 每种方法都有其优点和缺点:

成分:

  • 无需其他类型即可构建它们
  • 没有使用Functor / Applicative / Monad类合并效果的通用方法
  • 一切都很棒,直到涉及单子

变形金刚:

  • 允许您将多种效果合并为一个
  • 但是您需要一个单独的类型(通常是一些newtype
  • 使用提升,您可以在变换堆栈的任何层上执行计算。
  • 但是,尽管有专门的功能,但您不能单独考虑效果

变压器与离合器的组成有所不同(我不知道如何称呼它)。 有一些构图后,您可以将其变成变压器,反之亦然。 对接方案将对此有所帮助。

对接方案


如果我们仔细查看monad变压器的类型,我们可以确定一些模式:

 newtype ReaderT rma = ReaderT { runReaderT :: r -> ma } newtype MaybeT ma = MaybeT { runMaybeT :: m (Maybe a) } newtype ExceptT ema = ExceptT { runExceptT :: m (Either ea)) } newtype StateT sma = StateT { runStateT :: s -> m (a,s) } 

变压器描述了一种特殊情况,即当前的确定和不确定效果应如何啮合。

t为确定而u为不确定,请尝试:

 Reader: r -> ua ===> (->) r :. u := a ===> t :. u := a -- t ~ (->) r Maybe: u (Maybe a) ===> u :. Maybe := a ===> u :. t := a -- t ~ Maybe Either: u (Either ea) ===> u :. Either e := a ===> u :. t := a -- t ~ Either e 

一些效果非常复杂,可以通过其他更简单的效果来定义:

 State: s -> u (a, s) ===> (->) s :. (,) s := a ==> t :. u :. t' := a -- t ~ (->) s, t' ~ (,) s newtype State sa = State ((->) s :. (,) s := a) 

如果我们仔细看一下前三个示例,我们会注意到常见的模式:如果在Reader中 ,某种效果包裹了一个不确定的效果(将其放在方括号中,成为函子的对象),然后使用EitherMaybe相反,一个不确定的效果包裹了一个特定的效果。 在State的情况下我们甚至将函子放在两个更简单定义的效果之间。

让我们尝试以类型表示这些模式:

 newtype TU tua = TU (t :. u := a) newtype UT tua = UT (u :. t := a) newtype TUT tut' a = TUT (t :. u :. t' := a) 

我们刚刚定义了对接方案-这是包装器中函子的组成,表示特定和不确定效果的位置。

实际上,名称以run开头的转换器的方法只是删除了转换器的包装,返回了函子的组成。 我们描述这种类型的类型:

 class Composition t where type Primary ta :: * run :: ta -> Primary ta 

现在,我们有了运行这些电路的通用方法:

 instance Composition (TU tu) where type Primary (TU tu) a = t :. u := a run (TU x) = x instance Composition (UT tu) where type Primary (UT tu) a = u :. t := a run (UT x) = x instance Composition (TUT tu t') where type Primary (TUT tu t') a = t :. u :. t' := a run (TUT x) = x 

那变压器呢? 在这里,您还需要一个类型类,其中为特定类型指定了对接方案,声明了embed方法以将不确定的影响提高到转换器的级别,并构建为将特定的效果构造到转换器中:

 class Composition t => Transformer t where type Schema (t :: * -> *) (u :: * -> *) = (r :: * -> *) | r -> tu embed :: Functor u => u ~> Schema tu build :: Applicative u => t ~> Schema tu type (:>) tua = Transformer t => Schema tua 

现在剩下的要声明实例了,从MaybeEither开始:

 instance Transformer Maybe where type Schema Maybe u = UT Maybe u embed x = UT $ Just <$> x build x = UT . pure $ x instance Transformer (Either e) where type Schema (Either e) u = UT (Either e) u embed x = UT $ Right <$> x build x = UT . pure $ x 

我们将为Reader创建我们自己的类型,因为它不在基础中 。 而且他还需要Composition类的一个实例,因为它是箭头函子的包装器:

 newtype Reader ea = Reader (e -> a) instance Composition (Reader e) where type Primary (Reader e) a = (->) ea run (Reader x) = x instance Transformer (Reader e) where type Schema (Reader e) u = TU ((->) e) u embed x = TU . const $ x build x = TU $ pure <$> run x 

State做类似的事情:

 newtype State sa = State ((->) s :. (,) s := a) instance Composition (State s) where type Primary (State s) a = (->) s :. (,) s := a run (State x) = x instance Transformer (State s) where type Schema (State s) u = TUT ((->) s) u ((,) s) embed x = TUT $ \s -> (s,) <$> x build x = TUT $ pure <$> run x 

举个例子


仍然需要在现实世界中的问题上进行测试-例如,我们将编写一个程序来计算各种类型支架的正确位置。

定义括号的类型:它们可以是开和关的; 并且也有不同的风格:

 data Shape = Opened | Closed data Style = Round | Square | Angle | Curly 

我们程序的其他符号并不有趣:

 data Symbol = Nevermind | Bracket Style Shape 

我们还定义了程序可能遇到的错误列表:

 data Stumble = Deadend (Int, Style) --     | Logjam (Int, Style) --     | Mismatch (Int, Style) (Int, Style) --       

我们的程序需要什么效果? 我们需要保留一个括号列表以等待验证,并且需要在遇到第一个错误时停下来。 我们制造一个变压器:

 State [(Int, Style)] :> Either Stumble := () 

该算法很简单:我们遍历带有索引方括号的结构,如果通过之后我们没有遇到错误,但是仍然处于状态,则打开的方括号没有封闭的方括号:

 checking :: Traversable t => t (Int, Symbol) -> Either Stumble () checking struct = run (traverse proceed struct) [] >>= \case (s : _, _) -> Left . Logjam $ s where ([], _) -> Right () 

我们记得任何开放式括号,将任何封闭式与最后记住的开放式进行比较:

 proceed :: (Int, Symbol) -> State [(Int, Style)] :> Either Stumble := () proceed (_, Nevermind) = pure () proceed (n, Bracket style Opened) = build . modify . (:) $ (n, style) procceed (n, Bracket closed Closed) = build get >>= \case []-> embed $ Left . Deadend $ (n, closed) ((m, opened) : ss) -> if closed /= opened then embed . Left $ Mismatch (m, opened) (n, closed) else build $ put ss where 

结论


使用具有某些函子组成的对接方案,我们可以将它们变成transfomers,反之亦然。 不幸的是,这种技巧不适用于monads的母亲-续集。 所有这些都是因为无法想象它们是函子的组成,但是有可能是函子的组成...但是,这是一个完全不同的故事。

Github上的库代码 | 黑客文档 | 括号示例

Source: https://habr.com/ru/post/zh-CN467683/


All Articles