用于处理多项记录的 API

问题描述

这是一个有点自定义的问题,不是人为的,只是尽可能地简化。

-- this record that has fn that handles both x and y,-- x and y supposed to be Functors,a arbitrary param for x/y,r is arbitrary result param
type R0 a x y r =   
  { fn :: x a -> y a -> r
  }

-- this record that has fn that handles only x
type R1 a x r = 
  { fn :: x a -> r
  }

我想要的是一个可以处理 R0 和 R1 类型值的通用 API(函数)。

所以我做了一个和类型

data T a x y r  
  = T0 (R0 a x y r)  
  | T1 (R1 a x r)

并且我声明了这个函数,有一个约束,即 xy 必须是函子。

some :: ∀ a x y r.
  Functor x =>  
  Functor y =>
  T a x y r -> a
some = unsafeCoerce -- just stub

然后尝试使用它。

data X a = X { x :: a}
data Y a = Y { y :: a }

-- make X type functor
instance functorX :: Functor X where
  map fn (X val) = X { x: fn val.x } 

-- make Y type functor
instance functorY :: Functor Y where
  map fn (Y val) = Y { y: fn val.y }

-- declare functions
fn0 :: ∀ a. X a -> Y a -> Unit
fn0 = unsafeCoerce

fn1 :: ∀ a. X a -> Unit
fn1 = unsafeCoerce

尝试申请some

someRes0 = some $ T0 { fn: fn0 } -- works

someRes1 = some $ T1 { fn: fn1 } -- error becase it can not infer Y which should be functor but is not present in f1.

所以问题是:是否有可能以一种合理/符合人体工程学的方式使此类 API 工作(不需要此 API 的用户提供一些附加类型注释)?

我显然可以实现不同的函数 some0some1 来处理这两种情况,但我想知道使用单个函数(这使 API 表面更简单)的方式是否可行。

还有什么其他建议可以实现这些要求(当其中一个记录具有过多参数时,良好的 API 处理这种以上述方式不同的多态记录类型)?

解决方法

您应该使 T1T0 分开类型,然后使函数 some 本身重载以同时使用它们:

data T0 x y r a = T0 (R0 a x y r)
data T1 x r a = T1 (R1 a x r)

class Some t where
  some :: forall a. t a -> a

instance someT0 :: (Functor x,Functor y) => Some (T0 x y r) where
  some = unsafeCoerce

instance someT1 :: Functor x => Some (T1 x r) where
  some = unsafeCoerce

另一种虽然不太优雅的解决方案是让 some 的调用者使用类型签名显式指定 y 类型。这是编译器无法推断类型的情况下的默认方法:

someRes1 :: forall a. a
someRes1 = some (T1 { fn: fn1 } :: T a X Y Unit)

请注意,我必须为 someRes1 添加类型签名,以便在范围内包含类型变量 a。否则我无法在类型签名 T a X Y Unit 中使用它。


另一种指定 y 的替代方法是引入 FProxy 类型的虚拟参数:

some :: ∀ a x y r.
  Functor x =>
  Functor y =>
  FProxy y -> T a x y r -> a
some _ = unsafeCoerce

someRes0 = some FProxy $ T0 { fn: fn0 }

someRes1 = some (FProxy :: FProxy Maybe) $ T1 { fn: fn1 }

这样您就不必拼出 T 的所有参数。


我提供了后两个解决方案只是为了上下文,但我相信第一个是您正在寻找的,基于您对提到“多态方法”的问题的描述。这就是类型类的用途:它们引入了临时多态性。


说到“方法”:根据这个词,我猜那些 fn 函数来自某个 JavaScript 库,对吧?如果是这样的话,我相信你做错了。将 PureScript-land 类型泄漏到 JS 代码中是不好的做法。首先,JS 代码可能会意外损坏它们(例如通过变异),其次,PureScript 编译器可能会在不同版本之间更改这些类型的内部表示,这会破坏您的绑定。

更好的方法是始终根据原语(或专门用于 FFI 交互的类型,例如 the FnX family)来指定 FFI 绑定,然后使用一层 PureScript 函数来转换 PureScript 类型这些原语的参数并将它们传递给 FFI 函数。