更改 Haskell 的元编程函子

Changing Haskell's Functor for Metaprogramming

我对范畴论的了解不是很好。所以请多多包涵。

我一直在读书Monads Made Difficult

看到了下面的定义

class (Category c, Category d) => Functor c d t where
  fmap :: c a b -> d (t a) (t b)

从那个网站上我读到 Haskell 前奏中的 Functor 类型实际上是一个 Endofunctor。 (上面class中的c类和d类都是Hask)

看完页面后,我在想。如果使用真正的 Functors 而不仅仅是 Endofunctors,Haskell 会更适合元编程吗?

假设我们有以下内容(Js 代表 Javascript)

data Js a where
  litInt :: Integer -> Js Integer
  litString :: String -> Js String
  var :: String -> Js a
  lam :: String -> Js b -> Js (a -> b)
  let :: String -> Js a -> Js b -> Js b
  ap :: JsFunc a b -> Js a -> Js b

type JsFunc a b = Js (a -> b)

现在回到上面Functor的定义

class (Category c, Category d) => Functor c d t where
  fmap :: c a b -> d (t a) (t b)

我们可以让c是JsFunc,d是Hask,t是Js

这将为我们提供 Js 的 Functor。我无法使用 Haskell 的 Endofunctor。

我找不到 Apply 或 Applicative 类型的任何内容 -classes 以与此 Functor 相同的方式定义。那些也可以做同样的事情吗?

如果我错了,请纠正我。但是同样不能对 Monad 做,因为它真的是一个 Endofunctor 的实例?

根据 this mailing list post, applicative functors are known in the math literature as "weakly symmetric lax monoidal functors". A (brief, not careful) look at the nlab page for lax monoidal 表明这个概念也由两个类别参数化,因此您可以通过一些工作将此参数仿函数 class 扩展为参数应用程序 class。

正如你所说,monads是endofunctors,所以它们不能被两个类别参数化。但是他们作为参数的一类不一定是 Hask;所以也可以给出一个参数化的 monad,比如

class Functor c c f => Monad c f where
    return :: c a (f a)
    join :: c (f (f a)) (f a)

然后我们可以使用一种标准技巧来定义绑定:

(=<<) :: Monad c m => c a (m b) -> c (m a) (m b)
(=<<) f = join . fmap f

这里的(.)操作是Categoryclass的组合,不是Prelude.

的函数组合

嗯,首先——

... if it used real Functors and not just Endofunctors ...

Haskell 仿函数 真正的仿函数。只是,Functor class 不允许 任何 种通用函子,而只允许在 Hask[= 上作为内函子的特定函子48=].

确实,非内函函子很有趣;数学家一直在使用它们。正如你所说,不可能有非内函子单子,类似于 Applicative 可能: class 真的只是一个很好的 Haskell接口为monoidal functors,可以在不同类别之间定义。看起来像这样:

class (Functor r t f, Category r, Category t) => Monoidal r t f where
  pureUnit :: () `t` f ()
  fzipWith :: (a, b) `r` c -> (f a, f b) `t` f c

要真正像标准 Applicative class 一样方便地在任何地方使用它,您需要的不仅仅是 Monoidal class。这已经在 couple of libraries. I also wrote one 中进行了尝试。但是,嗯...它看起来并不漂亮...

class (Functor f r t, Cartesian r, Cartesian t) => Monoidal f r t where
  pureUnit :: UnitObject t `t` f (UnitObject r)
  fzipWith :: (ObjectPair r a b, Object r c, ObjectPair t (f a) (f b), Object t (f c))
              => r (a, b) c -> t (f a, f b) (f c)

class (Monoidal f r t, Curry r, Curry t) => Applicative f r t where
  -- ^ Note that this tends to make little sense for non-endofunctors. 
  --   Consider using 'constPure' instead.
  pure :: (Object r a, Object t (f a)) => a `t` f a 

  (<*>) :: ( ObjectMorphism r a b
           , ObjectMorphism t (f a) (f b), Object t (t (f a) (f b))
           , ObjectPair r (r a b) a, ObjectPair t (f (r a b)) (f a)
           , Object r a, Object r b )
       => f (r a b) `t` t (f a) (f b)
  (<*>) = curry (fzipWith $ uncurry id)

我不知道这些框架中的任何一个是否允许您以实用的方式编写您想要的应用程序 JS 代码。有可能,但实际上我宁愿怀疑它会变得 非常 混乱。另请注意,虽然您现在有应用程序浮动,但这并不意味着您实际上可以在 Js 代码中执行通常使用 Hask 应用程序所做的事情——相反,您实际上需要将这些应用程序作为转换器包装在 Js 类型周围。

您可能要考虑完全放弃“JS 价值观”,而 以 category/arrow 范式代替。即,改变定义:

data JsFunc a b where
    litInt :: Integer -> JsFunc () Integer
    litString :: String -> JsFunc () String
    var :: String -> JsFunc () a
    lam :: String -> JsFunc () b -> JsFunc a b
    let :: String -> JsFunc () a -> JsFunc () b -> JsFunc () b
    ap :: JsFunc a b -> JsFunc () a -> JsFunc () b

type Js = JsFunc ()

在这里,我们使用 () 作为 terminal object of the JsFunc category – which makes the Js a ≡ JsFunc () a arrows equivalent to what we normally call values (at least if we don't consider strictness semantics). If you go that route, pretty much everything needs to be written point-free, but there's syntax to pretend otherwise,您可以很好地合并函子甚至 monad(如 Kleisli 箭头)。

we can have c be JsFunc, d be Hask and have t be Js

嗯,不,我们不能,因为 JsFunc 是一个不饱和类型的同义词,所以我们不能为它实例化一个变量 (c)。

您可以创建一个新类型

newtype JsCat a b = JsCat (Js (a -> b))

然后像你说的那样创建一个Functor JsCat (->) Js

但是,这不会给您带来任何新的东西,因为如果我们扩展您想要的 fmap 类型,它就会变成

fmap :: JsFunc a b -> (->) (Js a) (Js b) -- or
fmap :: Js (a -> b) -> Js a -> Js b

这是 none 而不是 JsApply 实例。所以,我不确定你为什么要把它变成一个仿函数,因为它已经适合现有的抽象。