如何在Haskell中按模块参数化函数?

如何在Haskell中按模块参数化函数?,haskell,haskell-backpack,Haskell,Haskell Backpack,这似乎是人为的,但我似乎找不到以下问题的明显答案: mapComp :: KVPairs -> IO () mapComp kvpairs = do let init = M.empty let m = foldr ins init kvpairs where ins (k, v) t = M.insert k v t if M.size m /= length kvpairs then putStrLn $ "FAIL: " ++ show (M.size

这似乎是人为的,但我似乎找不到以下问题的明显答案:

mapComp :: KVPairs -> IO ()
mapComp kvpairs = do
  let init = M.empty
  let m = foldr ins init kvpairs where
        ins (k, v) t = M.insert k v t
  if M.size m /= length kvpairs
  then putStrLn $ "FAIL: " ++ show (M.size m) ++ ", " ++ show (length kvpairs)
  else pure ()

hashmapComp :: KVPairs -> IO()
hashmapComp kvpairs = do
  let init = HML.empty
  let m = foldr ins init kvpairs where
        ins (k, v) t = HML.insert k v t
  if HML.size m /= length kvpairs
  then putStrLn $ "Fail: " ++ show (HML.size m) ++ ", " ++ show (length kvpairs)
else pure ()
假设我有以下导入:

import qualified Data.Map as M
import qualified Data.HashMap.Lazy as HML
现在我有了一些函数(
comp
),它获取一些列表,做一些事情,创建一个映射,然后返回它

我的问题是如何有两种调用
comp
的方法,以便它对
insert
size
的调用正确映射

作为一个稻草人,我可以编写这个函数的两个副本,一个引用
M.insert
M.size
,另一个引用
HML.insert
HML.size
。。。但我如何“将模块作为参数传递”,或者以其他方式表示

谢谢

编辑:为了减少抽象性,以下是
comp
的精确定义:

mapComp :: KVPairs -> IO ()
mapComp kvpairs = do
  let init = M.empty
  let m = foldr ins init kvpairs where
        ins (k, v) t = M.insert k v t
  if M.size m /= length kvpairs
  then putStrLn $ "FAIL: " ++ show (M.size m) ++ ", " ++ show (length kvpairs)
  else pure ()

hashmapComp :: KVPairs -> IO()
hashmapComp kvpairs = do
  let init = HML.empty
  let m = foldr ins init kvpairs where
        ins (k, v) t = HML.insert k v t
  if HML.size m /= length kvpairs
  then putStrLn $ "Fail: " ++ show (HML.size m) ++ ", " ++ show (length kvpairs)
  else pure ()

编辑(2):这比我预期的要有趣得多,感谢所有回应的人

如果没有解决办法,恐怕在哈斯克尔是不可能做到的。主要问题是,
comp
会对
M
HML
变体的相同对象使用不同的类型,这在Haskell中是不可能直接实现的

您需要让
comp
知道使用数据或多态性选择哪个选项

作为一个基本想法,我将创建ADT以涵盖可能的选项,并使用布尔值确定模块:

data SomeMap k v = M (M.Map k v) | HML (HML.HashMap k v)
f :: Bool -> IO ()
f shouldIUseM = do ...
然后使用
foldr
中的
case
表达式检查基础映射是
M
还是
HML
。但是,我不认为使用这样的膨胀代码有什么好处,最好分别创建
compM
compHML

另一种方法是创建一个typeclass来包装所有的案例

class SomeMap m where
  empty :: m k v
  insert :: k -> v -> m k v -> m k v
  size :: m k v -> Int
然后手动为每个地图编写实例(或者使用一些TemplateHaskell魔术,我相信这在这里会有所帮助,但这不是我的技能)。它还需要一些膨胀代码,但随后您将能够在使用的贴图类型上参数化
comp

comp :: SomeMap m => m -> IO ()
comp thisCouldBeEmptyInitMap = do ...
但老实说,我会这样写这个函数:

comp :: Bool -> IO ()
comp m = if m then fooM else fooHML

最简单的方法是通过实际需要的操作而不是模块来参数化。因此:

mapComp ::
  m ->
  (K -> V -> m -> m) ->
  (m -> Int) ->
  KVPairs -> IO ()
mapComp empty insert size kvpairs = do
  let m = foldr ins empty kvpairs where
        ins (k, v) t = insert k v t
  if size m /= length kvpairs
  then putStrLn $ "FAIL: " ++ show (size m) ++ ", " ++ show (length kvpairs)
  else pure ()
然后可以将其称为,例如
mapcomm.empty M.insert M.size
mapcomm.empty HM.insert HM.size
。作为一个小的附带好处,调用方可以使用此函数,即使他们喜欢的数据结构没有通过编写小型适配器并将其传入来提供具有正确名称和类型的模块

如果愿意,您可以将这些内容合并到一个记录中,以便于传递:

data MapOps m = MapOps
    { empty :: m
    , insert :: K -> V -> m -> m
    , size :: m -> Int
    }

mops = MapOps M.empty M.insert M.size
hmops = MapOps HM.empty HM.insert HM.size

mapComp :: MapOps m -> KVPairs -> IO ()
mapComp ops kvpairs = do
    let m = foldr ins (empty ops) kvpairs where
          ins (k, v) t = insert ops k v t
    if size ops m /= length kvpairs
    then putStrLn "Yikes!"
    else pure ()

我有点怀疑这是一个XY问题,下面是我将如何处理您链接到的代码。你有以下几点:

mapComp :: KVPairs -> IO ()
mapComp kvpairs = do
  let init = M.empty
  let m = foldr ins init kvpairs where
        ins (k, v) t = M.insert k v t
  if M.size m /= length kvpairs
  then putStrLn $ "FAIL: " ++ show (M.size m) ++ ", " ++ show (length kvpairs)
  else pure ()

hashmapComp :: KVPairs -> IO()
hashmapComp kvpairs = do
  let init = HML.empty
  let m = foldr ins init kvpairs where
        ins (k, v) t = HML.insert k v t
  if HML.size m /= length kvpairs
  then putStrLn $ "Fail: " ++ show (HML.size m) ++ ", " ++ show (length kvpairs)
else pure ()
这有很多重复,这通常是不好的。因此,我们计算出两个函数之间不同的位,并通过这些变化的位参数化一个新函数:

正如你所看到的,这是一个真正的机械过程。然后调用例如
comp M.empty M.insert M.size

如果您希望能够定义
comp
,以便它可以处理您尚未想到的地图类型(或用户将指定的地图类型),则必须针对抽象界面定义
comp
。这是通过TypeClass完成的,如
SomeMap
radrow的答案

事实上,您已经可以完成此抽象的一部分了,注意到您想要使用的两个映射都实现了标准的
Foldable
Monoid

-- didn't try to compile this
comp :: (Foldable (mp k), Monoid (mp k v))=> (k -> v -> mp k v -> mp k v) -> KVPairs -> IO()
comp h_insert kvpairs = do
  let init = mempty -- ...also why not just use `mempty` directly below:
  let m = foldr ins init kvpairs where
        ins (k, v) t = h_insert k v t
  if length m /= length kvpairs
  then putStrLn $ "Fail: " ++ show (length m) ++ ", " ++ show (length kvpairs)
else pure ()

正如在评论中提到的,我认为是(将是?)获得我认为您需要的东西的方法,即参数化模块。我对它了解不多,也不清楚它解决了哪些用例,您不想使用我上面描述的更传统的方法(也许我会阅读wiki页面)。

以下是如何使用模块签名和混合(也称为

您必须定义一个库(它可以是一个内部库),类似于:

在同一个库或另一个库中,编写导入签名的代码,就像导入普通模块一样:

module Stuff where

import qualified Mappy as M

type KVPairs k v = [(k,v)]

comp :: M.C k => KVPairs k v -> IO ()
comp kvpairs = do
  let init = M.empty
  let m = foldr ins init kvpairs where
        ins (k, v) t = M.insert k v t
  if M.size m /= length kvpairs
  then putStrLn $ "FAIL: " ++ show (M.size m) ++ ", " ++ show (length kvpairs)
  else pure ()
在另一个库(必须是另一个库)中编写与签名匹配的“实现”模块:

-- file Mappy.hs
{-# language ConstraintKinds #-}
module Mappy (C,insert,empty,size,Map) where

import Data.Map.Lazy

type C = Ord
“签名匹配”仅基于名称和类型执行,实现模块不需要知道签名的存在

然后,在要使用抽象代码的库或可执行文件中,使用抽象代码拉取库,使用实现拉取库:

executable somexe
  main-is:             Main.hs
  build-depends:       base ^>=4.11.1.0,
                       indeflib,
                       lazyimpl
  default-language:    Haskell2010

library indeflib
  exposed-modules:     Stuff
  signatures:          Mappy
  build-depends:       base ^>=4.11.1.0
  hs-source-dirs:      src
  default-language:    Haskell2010

library lazyimpl
  exposed-modules:     Mappy
  build-depends:       base ^>=4.11.1.0,
                       containers >= 0.5
  hs-source-dirs:      impl1
  default-language:    Haskell2010
有时签名和实现模块的名称不匹配,在这种情况下,必须使用Cabal文件的部分

编辑。创建
HashMap
实现被证明有些棘手,因为需要两个约束(
Eq
Hashable
)而不是一个约束。我不得不采取这种伎俩。代码如下:

{-# language ConstraintKinds, FlexibleInstances, UndecidableInstances #-}
module Mappy (C,insert,HM.empty,HM.size,Map) where

import Data.Hashable
import qualified Data.HashMap.Strict as HM

type C = EqHash 

class (Eq q, Hashable q) => EqHash q -- class synonym trick
instance (Eq q, Hashable q) => EqHash q

insert :: EqHash k => k -> v -> Map k v -> Map k v
insert = HM.insert

type Map = HM.HashMap

如果我理解正确,我认为您只需要传递某种类型的参数(一个
Bool
或一些特殊构造的自定义数据类型)来指示要使用哪个模块。也许其他人会知道一些更复杂的方法。@RobinZigmond我明白你的意思,你的意思是
let insertfn=if-then M.insert else HML.insert
(同样对于
sizefn
),然后根据需要使用
insertfn
?我想这是可行的,但我想知道当我有两个以上可能的模块时,是否有一种惯用的方法来实现这一点。是的,这就是我的意思。正如我所说,我不知道是否有一种“更好”的方法可以做到这一点——我认为这是不可能的,但让我们等待专家的评论/回答。不能。一个设计良好的typeclass接口通常能解决同样的问题,但并不总是如此。另请参见“backpack”,它是一个类似于ML函子的模块ext
{-# language ConstraintKinds, FlexibleInstances, UndecidableInstances #-}
module Mappy (C,insert,HM.empty,HM.size,Map) where

import Data.Hashable
import qualified Data.HashMap.Strict as HM

type C = EqHash 

class (Eq q, Hashable q) => EqHash q -- class synonym trick
instance (Eq q, Hashable q) => EqHash q

insert :: EqHash k => k -> v -> Map k v -> Map k v
insert = HM.insert

type Map = HM.HashMap