Purescript 将 cons 定义为类型类运算符

Purescript define cons as typeclass operator

Cons operator is defined (:) is defined for Array (Array.cons) and List (Cons type constructor).所以要在代码中使用它,我们应该:

import Data.List ((:))

import Data.Array ((:))

我想知道是否可以定义(:),以便它可以在一个模块中导入并在同一个模块中用于数组和列表。

我试过这样做:

class Cons container element where
  cons :: element -> container -> container

instance consArray :: Cons (Array a) a where
  cons = Array.cons

instance constList :: Cons (List a) a where
  cons = List.Cons

infixr 6 cons as :

它似乎适用于基本情况:

arr :: Array Int
arr = 1 : [2,3]

lst :: List Int
lst = 1 : (2 :3 : Nil)

但有些高级案例,例如:

data X a = X a

getX :: ∀ msg. Int -> X msg
getX = unsafeCoerce

getCons :: ∀ msg. 
  Array (X msg) ->
  Array (X msg)
getCons children = getX 1 : children

但它给出了错误:

No type class instance was found for

    MyModule.Cons (Array (X msg2))
                       (X t3)

  The instance head contains unknown type variables. Consider adding a type annotation.

while applying a function cons
  of type Cons t0 t1 => t1 -> t0 -> t0
  to argument getX 1
while inferring the type of cons (getX 1)
in value declaration getCons

where msg2 is a rigid type variable
        bound at (line 0, column 0 - line 0, column 0)
      t0 is an unknown type
      t1 is an unknown type

所以问题是通常有可能实现我所描述的吗?

UPD:

这个:

class Cons container where
  cons :: forall element. element -> container element -> container element

解决构造Array或List时统一使用(:)的问题。

所以,在导入这个 (:) 之后,我们可以在同一个模块中做:

arrFn :: ∀ a. a -> Array a -> Array a
arrFn el array = el : array

listFn :: ∀ a. a -> List a -> List a
listFn el list = el : el : list


-- and even this will work too:

data X a = X a

getX :: ∀ msg. Int -> X msg
getX = unsafeCoerce

getCons :: ∀ msg. 
  Array (X msg) ->
  Array (X msg)
getCons children = getX 1 : children

问题在于以这种方式定义的操作符不能用于模式匹配(因为模式处理只适用于类型构造函数),这将给出错误:

matchList :: List Int -> Int
matchList ls =
   case ls of
     Nil -> 0
     (x : xs) -> x 

似乎不​​可能实现 (:) 的完全通用使用,因此它适用于 Array/List 构造和 List 模式匹配。

TL;DR: 你需要一个函数依赖。

长答案

发生这种情况是因为编译器不知道应该查找 Cons 的哪个实例。

在表达式getX 1 : children中,编译器知道children :: Array (X msg),但是getX 1的类型是什么?函数 getX 可以 return X 任何类型,任何类型。那么应该是X Int?还是X String?或者,也许 X Boolean?没有办法告诉!因此,编译器只是为一些未知的 t3 调用该类型 X t3,并从那里继续,希望 t3 会及时为人所知。

但事实并非如此。编译器必须解决的下一件事是应用 (:) 运算符。为此,它需要找到一个实例 Cons (Array (X msg)) (X t3),但它不知道如何找到,因为它不知道 t3 是什么,并且现有实例的 none 匹配那个形状。


顺带一提,你可以到此为止,验证一下。将您的实例更改为此:

instance consArray :: Cons (Array a) b where
  cons _ xs = xs

之后,getCons突然编译。为什么?因为在替换 a ~ X msgb ~ X t3 时,新实例头 Cons (Array a) b 实际上确实匹配所需的 Cons (Array (X msg)) (X t3)。而且它从来没有真正出现 t3 实际上是什么,所以它可以被忽略。

此外,您甚至不需要 X。您可以重现问题:

getX :: forall a. Int -> a
getCons :: forall msg. Array msg -> Array msg

X只会混淆视听


但是“通常有可能实现我所描述的”吗?

好吧,您还没有真正描述您想要的结果,因此很难确定。但是如果我不得不 猜测 ,在我看来你真正想要的是让编译器弄清楚,因为 (:) 的第二个参数是 Array (X msg),则第一个参数必须是该数组的一个元素 - 即 X msg,- 然后使用该信息推断此实例化中 getX 的预期类型。

如果这确实是您想要实现的目标,那么您需要的是 functional dependency:

class Cons container element | container -> element where
                             ^^^^^^^^^^^^^^^^^^^^^^
                                    |
                               this bit here

这段语法告诉编译器,如果 container 以某种方式已知,那么 element 也必须已知。

实际上它有两个作用:

  1. 您不能声明任何具有相同 container 但不同 element 的实例。例如,Cons (Array Int) IntCons (Array Int) String 将不起作用。编译器会抱怨它们违反了函数依赖性。
  2. 但是在 return 中,编译器现在可以通过知道 container.
  3. 来推断 element

因此,如果您只添加该位,getCons 将编译正常:编译器将首先理解 container ~ Array (X msg),因此它会选择匹配的实例 - Const (Array a) a,-并从那里推断出 element ~ X msg,因此 getX :: Int -> X msg.


旧答案 - 在问题发生实质性改变之前写的

这与在类型 class 中定义运算符无关。如果您只是从 Data.Array 导入运算符,则会发生完全相同的事情,只是错误消息会好一点。

问题是您试图将类型 X a 的值与类型 Tuple String (X a) 的值数组相结合。类型不同。 Tuple String (X a)X a 不同。

要使其正常工作,您必须修复类型签名以使类型相同:

xFn :: ∀ a. Tuple String (X a) -> Array (Tuple String (X a)) -> Array (Tuple String (X a))
xFn el list = el : list

或者你必须在尝试使用它之前从 el 构造一个元组:

xFn :: ∀ a. X a -> Array (Tuple String (X a)) -> Array (Tuple String (X a))
xFn el list = Tuple "foo" el : list

无论哪种方式,元素都必须在某个时刻转换为元组。哪种方式“正确”取决于您的具体情况。


或者,您也可以提供一个实例 Cons (Array (Tuple String a)) a,并将元素转换为该实例中的元组。缺少这样的实例是编译器在错误消息中抱怨的。它可能看起来像这样:

instance consTuple :: Cons (Array (Tuple String a)) a where
    cons a xs = cons (Tuple "foo" a) xs

除非这样的实例不起作用,因为它会与 consArray 重叠。所以你可以通过将它们放在一个链中来修复 that:

instance consArray :: Cons (Array a) a where
  cons = Array.cons
else instance consTuple :: Cons (Array (Tuple String a)) a where
  cons a xs = cons (Tuple "foo" a) xs

尽管我无法想象您对此有什么可能的用例。