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

时间:2019-03-07 22:39:31

标签: haskell haskell-backpack

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

说我有以下进口商品:

import qualified Data.Map as M
import qualified Data.HashMap.Lazy as HML

现在我有了一些函数(comp),该函数可以获取一些列表,执行某些操作,创建地图并返回。

我的问题是我如何有两种调用comp的方式,以便其对insertsize的调用(例如)正确映射?

作为一名稻草人,我可以编写此函数的两个副本,一个引用M.insertM.size,而另一个引用HML.insertHML.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):事实证明,这比我预期的要有趣得多,这要归功于所有人的回答!

4 个答案:

答案 0 :(得分:6)

以下是使用模块签名和混合插件(也称为Backpack)的方法

您必须使用signature定义一个库(它可能是一个内部库):

-- file Mappy.hsig
signature Mappy where

class C k
data Map k v
empty :: Map k v
insert :: C k => k -> v -> Map k v -> Map k v 
size :: Map k v -> Int

在同一个库中或另一个库中,编写将签名当作普通模块导入的代码:

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文件的mixins部分。

编辑。创建HashMap实现有些棘手,因为insert需要两个约束(EqHashable)而不是一个约束。我不得不求助于"class synonym"技巧。这是代码:

{-# 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

答案 1 :(得分:5)

最简单的方法是根据实际需要的操作而不是模块进行参数化。所以:

mapComp M.empty M.insert M.size

然后您可以将其称为,例如mapComp HM.empty HM.insert HM.sizedata 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 () 。作为一个小的附带好处,即使调用者喜欢的数据结构没有通过编写小型适配器并将其传递来提供名称和类型正确的模块,也可以使用此功能。

如果愿意,可以将它们合并为一条记录,以简化传递过程:

scale_fill_manual

答案 2 :(得分:1)

恐怕没有解决方法,就无法在Haskell中进行操作。主要问题是power2 <- power_factory(2)会为compM的变体对相同的对象使用不同的类型,这在Haskell中是无法直接实现的。

您将需要让HML使用数据或多态性来选择哪个选项。

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

comp

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

另一种方法是创建将包装所有案例的类型类

compHML

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

comp

但是老实说,我会这样写这个函数:

comp :: SomeMap m => m -> IO ()
comp thisCouldBeEmptyInitMap = do ...

答案 3 :(得分:1)

我有点怀疑这是一个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 ()

这有很多重复,通常不好。因此,我们将两个函数之间不同的位分解出来,并通过这些更改的位参数化新功能

-- didn't try to compile this
comp :: mp k v -> (k -> v -> mp k v -> mp k v) -> (mp k v -> Int) -> KVPairs -> IO()
comp h_empty h_insert h_size kvpairs = do
  let init = h_empty
  let m = foldr ins init kvpairs where
        ins (k, v) t = h_insert k v t
  if h_size m /= length kvpairs
  then putStrLn $ "Fail: " ++ show (h_size m) ++ ", " ++ show (length kvpairs)
else pure ()

如您所见,这是一个真正的机械过程。然后您致电comp M.empty M.insert M.size

如果您希望能够定义comp,使其可以在尚未想到的地图类型上使用(或由用户指定),那么您可以必须针对抽象接口定义comp。这是通过类型类完成的,如radrow的答案SomeMap

实际上,您已经注意到要使用的两个映射都实现了标准FoldableMonoid,因此您已经可以做部分抽象了。

-- 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 ()

正如评论中提到的那样,我认为backpack是(将会是)获取我认为您要的东西的方法,即参数化的 modules 。我对此了解不多,对我来说不清楚它能解决什么用例,即您不想使用上面描述的更传统的方法(也许我会阅读Wiki页面)。