如何用类型类模拟Rust的Fn trait?解决Haskell动态函数类型问题
用Haskell实现类似Rust的动态函数类型方案
Rust中的函数抽象实现
Rust中没有专门的函数类型,而是通过Fn trait实现函数抽象,定义如下:
trait Fn<A: Tuple, R> { fn call(self, args: A) -> R; }
我们可以用dyn Fn(A) -> R表示动态函数类型,这种方式兼顾了编译器优化与使用便利性。
示例代码:
// Rust 示例 😊 fn run_with_2(f: &dyn Fn(i32) -> i32) -> i32 { f.call(2) } struct SomeFnWrapper<A,B>(Box<dyn Fn(A) -> B>);
Haskell的局限
在Haskell这类函数式语言中,可用类型类定义类似的Fn抽象:
class Fn s a b where call :: s -> a -> b
但Haskell的类型类没有Self概念,无法实现dyn Fn这类动态函数类型,只能全程依赖泛型:
-- Haskell 示例 🤮 runWith2 :: Fn f Int Int => f -> Int runWith2 f = call f 2 -- 在Haskell中无法创建`SomeFnWrapper`这样的类型
解决方案:借助forall实现动态函数包装
通过Haskell的全称量化forall,可以实现类型擦除,模拟出类似Rust的动态函数效果,核心思路是利用存在类型打包具体函数类型。
方式一:基于存在类型的包装器
需要开启ExistentialQuantification扩展:
{-# LANGUAGE ExistentialQuantification #-} -- 定义存在类型包装器,擦除具体函数类型,只保留约束 data SomeFn a b = forall f. Fn f a b => SomeFn f -- 为包装器实现Fn类型类,让它支持call方法 instance Fn (SomeFn a b) a b where call (SomeFn f) x = call f x -- 对应Rust的SomeFnWrapper newtype SomeFnWrapper a b = SomeFnWrapper (SomeFn a b) -- 接收动态函数的runWith2实现 runWith2 :: SomeFn Int Int -> Int runWith2 (SomeFn f) = call f 2 -- 测试示例 addOne :: Int -> Int addOne x = x + 1 main :: IO () main = do let wrapped = SomeFn addOne print $ runWith2 wrapped -- 输出3
方式二:基于秩2类型的简化写法
开启RankNTypes扩展后,可通过高阶函数直接实现类型擦除,无需额外定义存在类型:
{-# LANGUAGE RankNTypes #-} -- 定义动态函数类型,通过forall实现类型擦除 type DynFn a b = forall r. (forall f. Fn f a b => f -> r) -> r -- 接收DynFn的runWith2实现 runWith2' :: DynFn Int Int -> Int runWith2' fn = fn (\f -> call f 2) -- 使用示例 main' :: IO () main' = do let dynAddOne :: DynFn Int Int dynAddOne cont = cont addOne print $ runWith2' dynAddOne -- 输出3
总结
通过存在类型(结合ExistentialQuantification扩展)或秩2类型(结合RankNTypes扩展),Haskell完全可以实现类似Rust中dyn Fn的动态函数类型,打破只能全程使用泛型的局限。
内容的提问来源于stack exchange,提问作者דניאל פ.ח.
相关产品推荐
相关产品推荐

