在Haskell中实现非位置关键字参数

在Haskell中实现非位置关键字参数,haskell,types,type-theory,Haskell,Types,Type Theory,我正在尝试在Haskell中实现关键字参数,类似于Ocaml中的关键字参数。我的目标是使参数可以按任何顺序传递,并且可以部分应用于函数调用(生成一个新函数,该函数接受剩余的关键字参数) 我尝试使用DataTypes和一个表示“可以转换为函数a->b的值”的类型类来实现这一点。其思想是一个函数包含两个关键字参数,foo和bar,可以转换为类似于foo->bar->x的函数或类似于bar->foo->x的函数。只要foo和bar有不同的类型(而且x本身不是一个需要foo或bar的函数),这应该是明确

我正在尝试在Haskell中实现关键字参数,类似于Ocaml中的关键字参数。我的目标是使参数可以按任何顺序传递,并且可以部分应用于函数调用(生成一个新函数,该函数接受剩余的关键字参数)

我尝试使用DataTypes和一个表示“可以转换为函数
a->b
的值”的类型类来实现这一点。其思想是一个函数包含两个关键字参数,
foo
bar
,可以转换为类似于
foo->bar->x
的函数或类似于
bar->foo->x
的函数。只要
foo
bar
有不同的类型(而且
x
本身不是一个需要
foo
bar
的函数),这应该是明确的,这正是我用
Kwarg
GADT试图实现的

Fun
类中的函数依赖性旨在使
f
a
b
之间的关系更加明确,但我不确定这是否真的有帮助。我得到一个编译器错误,无论有没有它

我已启用以下语言扩展:

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE UndecidableInstances #-}
Quoter.hs的内容

module Quoter where

import GHC.TypeLits (Symbol)

data Kwarg :: Symbol -> * -> * where
  Value :: a -> Kwarg b a

class Fun f a b | f a -> b where
  runFun :: f -> a -> b

instance (Fun f' a' b) => Fun (a -> f') a' (a -> b) where
  runFun f a' = \a -> runFun (f a) a'

instance Fun (Kwarg name t -> b) (Kwarg name t) b where
  runFun = id
module Main where

import Quoter

test :: (Num a) => Kwarg "test" a -> Kwarg "some" a -> a
test (Value i) (Value j) = i + j

main = putStrLn $ show $
  runFun test (Value 5 :: Kwarg "some" Int) (Value 6 :: Kwarg "test" Int)
Main.hs的内容

module Quoter where

import GHC.TypeLits (Symbol)

data Kwarg :: Symbol -> * -> * where
  Value :: a -> Kwarg b a

class Fun f a b | f a -> b where
  runFun :: f -> a -> b

instance (Fun f' a' b) => Fun (a -> f') a' (a -> b) where
  runFun f a' = \a -> runFun (f a) a'

instance Fun (Kwarg name t -> b) (Kwarg name t) b where
  runFun = id
module Main where

import Quoter

test :: (Num a) => Kwarg "test" a -> Kwarg "some" a -> a
test (Value i) (Value j) = i + j

main = putStrLn $ show $
  runFun test (Value 5 :: Kwarg "some" Int) (Value 6 :: Kwarg "test" Int)
这是我在使用ghc构建时遇到的错误:

Main.hs:18:3: error:
    • Couldn't match type ‘Kwarg "some" Int -> b’ with ‘Int’
        arising from a functional dependency between:
          constraint ‘Fun (Kwarg "some" Int -> Int) (Kwarg "some" Int) Int’
            arising from a use of ‘runFun’
          instance ‘Fun (a -> f') a' (a -> b1)’ at <no location info>
    • In the second argument of ‘($)’, namely
        ‘runFun
           test (Value 5 :: Kwarg "some" Int) (Value 6 :: Kwarg "test" Int)’
      In the second argument of ‘($)’, namely
        ‘show
         $ runFun
             test (Value 5 :: Kwarg "some" Int) (Value 6 :: Kwarg "test" Int)’
      In the expression:
        putStrLn
        $ show
          $ runFun
              test (Value 5 :: Kwarg "some" Int) (Value 6 :: Kwarg "test" Int)

问题是您的实例重叠:

instance (Fun f' a' b) => Fun (a -> f') a' (a -> b) where

instance Fun (Kwarg name t -> b) (Kwarg name t) b
  -- this is actually a special case of the above, with a ~ a' ~ KWarg name t
如果我们用一个等价的关联类型族替换函数依赖项(这在我看来通常有点难以解释),就会变得更清楚:

{-# LANGUAGE TypeFamilies #-}

class Fun f a where
  type FRes f a :: *
  runFun :: f -> a -> FRes f a

instance Fun f' a' => Fun (a -> f') a' where
  type FRes (a -> f') a' = a -> FRes f' a'
  runFun f a' = \a -> runFun (f a) a'

instance Fun (Kwarg name t -> b) (Kwarg name t) where
  type FRes (Kwarg name t -> b) (Kwarg name t) = b
  runFun = id
在本例中,编译器消息非常清楚:

    Conflicting family instance declarations:
      FRes (a -> f') a' = a -> FRes f' a'
        -- Defined at /tmp/wtmpf-file10498.hs:20:8
      FRes (Kwarg name t -> b) (Kwarg name t) = b
        -- Defined at /tmp/wtmpf-file10498.hs:24:8
   |
20 |   type FRes (a -> f') a' = a -> FRes f' a'
   |        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
重叠实例是一个大问题,特别是当需要解决非平凡类型级别的函数时。然而,这并不是一个以前经常遇到的问题。解决方案是从基于实例子句的类型级函数(即FunDeps或关联的类型族)切换到:

要真正实现
runFun
,您仍然需要一个具有重叠实例的类,但是现在可以将这些实例与GHC pragmas一起使用:

class Fun f a where
  runFun :: f -> a -> FRes f a

instance {-# OVERLAPPABLE #-} (Fun f' a', FRes (a -> f') a' ~ (a->FRes f' a'))
               => Fun (a -> f') a' where
  runFun f a' = \a -> runFun (f a) a'

instance {-# OVERLAPS #-} Fun (Kwarg name t -> b) (Kwarg name t) where
  runFun = id
因此,您的测试现在可以工作了

不幸的是,它不会做它实际应该做的事情:允许改变参数顺序

main = putStrLn $ show
  ( runFun test (Value 5 :: Kwarg "some" Int) (Value 6 :: Kwarg "test" Int)
  , runFun test (Value 5 :: Kwarg "test" Int) (Value 6 :: Kwarg "some" Int) )
给予

本质问题是类型推断朝着不太有利的方向发展:从起始值到最终结果。这是大多数语言中唯一可用的方向,但在Haskell中,从最终结果推断到单个表达式几乎总是更好的,因为只有外部结果始终可用于与环境的统一。最后,这会让你想到

type family FFun a b where

i、 e.函数的类型由您想要的结果和您已经可以提供的参数决定。然后,根据手头的参数是否恰好是函数期望的第一个参数,就不会有重叠实例;相反,您应该建立某种类型的类型级映射,包括所有键控参数,并让函数接受一个这样的映射(或者,等价地,按字母顺序接受所有参数)。

您知道这个标准吗?这可能是一个更好的工程权衡:更少的实现工作,更少的麻烦使用。部分应用程序可以通过保留一些未初始化的字段来实现。您唯一要放弃的是编译器支持,它可以注意哪些字段仍然需要值。@DanielWagner是的,我知道这个技巧。我想看看这个实现作为概念证明的效果如何。(您可能已经从文件名
Quoter.hs
中猜到,它最初包含一个TH QUISQUOTER,以减少使用的麻烦。我已经删除了它,以使示例更加独立/适合StackOverflow,但我会在类型工作后再添加它。)编辑:编译器支持注意哪些字段仍然需要值,这是我想要的,因此实现如下:@leftaroundabout是否支持部分应用程序?我在博客上看不到任何关于这方面的讨论。我试图实现的是部分应用程序的可靠性检查(没有运行时异常)。除了实践方面,我还非常有兴趣理解为什么我试图做的事情不起作用的理论,并学习将使其起作用的理论。感谢您撰写本文。我仍在努力理解这一切。我同意实例是重叠的(事实上,我认为我的函数
test
有几个第一个实例的实例)。但我不认为这两个实例都是另一个实例的特例(即,每个实例的类型参数都不会与另一个实例的相应参数统一)。这种理解正确吗?(尽管我同意,即使是
测试
示例也会匹配这两个实例。)我会尝试理解类型族,看看这是否能让我更接近解决方案。关于类实例,一个经常被误解的事实是根本没有考虑约束列表来解决问题。也就是说,当编译器类型检查
runFun
的出现时,它只会看到
实例Fun(a->f')a'
实例Fun(Kwarg name t->b)(Kwarg name t)
。只有在决定选择前者之后,它才会继续尝试解决额外的
Fun f'a'
约束。因此你是对的–实际上,实例是不同的,但就实例解析而言,
Fun f'a'=>Fun(a->f')a'
更为一般。哦,我指的是关于原始实例的评论。这有三个参数。编译器是否不尝试统一所有参数(忽略任何约束)?(可能是因为FunDep?)我想我在最后的评论中回答了我自己的问题-FunDep意味着编译器
type family FFun a b where