在Haskell中,构建新类型(“包装类型”)的函数的惯用方法是什么?

4

StringWrapper1StringWrapper2成为两个包装字符串的类型(即newtype StringWrapper1 = StringWrapper1 Stringnewtype StringWrapper2 = StringWrapper2)。

现在假设我们正在尝试从StringWrapper1StringWrapper2构建一个函数。

funcWrapper :: StringWrapper1 -> StringWrapper2

一方面,我们希望明确传入该函数的是一个 StringWrapper1,因此我们不希望仅将 StringWrapper1 视为 String 的类型同义词(这会导致错误,我的经验可以证明)。另一方面,在概念上构建函数时,我们仍然以某种方式考虑 String。因此,我们要做的是首先构建 func,以便我们不必不断地包装和解包类型:
func :: String -> String

然后,我们使用 func 来构建 funcWrapper:
funcWrapper :: StringWrapper1 -> StringWrapper2
funcWrapper (StringWrapper1 str) = StringWrapper2 (func str)

问题/疑问: 这种写法是否符合惯用语?每个函数都要重复使用funcfuncWrapper看起来很别扭。Haskell是否提供了其他方法来解决这个问题,还是我漏掉了什么?或者我应该只使用类型同义词?


2
如果你在概念上考虑字符串,那么为什么需要类型级别的区分呢?不同类型的重点在于你从概念上考虑到了不同的东西,即使实际实现是一个字符串。 - leftaroundabout
1
我认为GeneralizedNewtypeDeriving通常比使用像coerce这样的东西更好(这里的答案似乎集中在这个上面),但这取决于情况。 - Alexis King
4个回答

5

正如其他人所说,你应该确保这确实是你想做的事情(请参见leftaroundabout的评论)。如果是的话,你可以使用标准库中的coerce来在具有相同运行时表示的类型之间进行转换:

func :: String -> String
func = ...

...

funcWrapper :: StringWrapper1 -> StringWrapper2
funcWrapper = coerce func

这似乎是最优雅的。它的使用被视为不良吗? - George
@George,我从未见过有人对此表示不满,如果这确实是您想要做的事情(通常您希望在某个时候新类型在外部是“不透明”的。通常在定义它的模块之外)。coerce也没有任何开销(这对于包装/解包解决方案并非如此),因为它在编译期间被消除。请注意,coerceunsafeCoerce非常不同。coerce始终是安全的。 - David Young
4
包装和解包方案也没有额外的开销。 - Benjamin Hodgson
4
@BenjaminHodgson 啊,没错。如果你有像 map (\(StringWrapper1 str) -> str) wrapperscoerce wrappers :: [String] 这样的东西,那么开销差异就会产生影响。 - David Young

3

首先,你应该考虑leftaroundabout的评论,并确保新的类型确实是有意义的。话虽如此,这种包装和解包在日常使用中确实很常见,但你可以使其更加方便。一种方法是利用字符串包装器作为单态函子(与多态的Functor相对),因此你可以编写映射函数,例如:

mapWrapper1 :: (String -> String) -> StringWrapper1 -> StringWrapper1
mapWrapper1 f (StringWrapper1 str) = StringWrapper1 (f str)

mapWrapper2 :: (String -> String) -> StringWrapper2 -> StringWrapper2
mapWrapper2 f (StringWrapper2 str) = StringWrapper2 (f str)

这种模式的一个著名推广是来自mono-traversable包的MonoFunctor类。
同时,定义两个包装器之间的转换函数也很容易(用花哨的术语来说,我们可以称其为两个函子之间的自然变换):
rewrap1as2 :: StringWrapper1 -> StringWrapper2
rewrap1as2 (StringWrapper1 str) = StringWrapper2 str

(rewrap1as2 可以简单地通过从 Data.Coerce 强制转换实现。有关详细信息,请参见 David Young's answer。)

user2297560's answer 中的 wrap 可以根据这些更基本的函数来定义:

mapAndRewrap1as2 :: (String -> String) -> StringWrapper1 -> StringWrapper2
mapAndRewrap1as2 f = rewrap1as2 . mapWrapper1 f

如果你想要更简洁的东西,你可能会喜欢 newtype 包,或者 lens 提供的等效 Isos。然而,这可能值得一个单独的答案。

2
为什么不编写一个函数来包装其他函数?
wrap :: (String -> String) -> StringWrapper1 -> StringWrapper2
wrap f (StringWrapper1 str) = StringWrapper2 (f str)

这将把任何 String -> String 转换为 StringWrapper1 -> StringWrapper2

这很优雅;但是,您仍将不得不为每种函数类型创建这些包装器函数。如果StringWrapper1嵌入到记录数据结构中怎么办?似乎仍然存在很多重复。 - George
但归根结底:你发帖中的解决方案是 Haskell 中惯用的方法吗? - George
1
@George 注意,然而“创建这些包装函数”相当于使用wrap f,而我会使用f。没有必要为所有这样的函数提供具有自己标识符的定义。我只会定义普通的f,并让调用者在需要时使用wrap f。没有重复。 - chi

1
使用newtype-generics包,可以编写类似以下内容的代码
{-# language DeriveGeneric #-}
module Main where

import GHC.Generics
import Control.Newtype (Newtype,over)

newtype StringWrapper1 = StringWrapper1 String deriving Generic

instance Newtype StringWrapper1

newtype StringWrapper2 = StringWrapper2 String deriving Generic

instance Newtype StringWrapper2

func :: String -> String
func = undefined

funcWrapper :: StringWrapper1 -> StringWrapper2
funcWrapper = over StringWrapper1 func

我不会定义包装函数,而是在每个站点上使用 over

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接