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

Haskell中在新类型(“包装类型”)上构建函数的惯用方法是什么?,haskell,Haskell,让StringWrapper1和StringWrapper2成为两种类型的字符串(即newtype-StringWrapper1=StringWrapper1-string和newtype-StringWrapper2=StringWrapper2) 现在假设我们正在尝试从StringWrapper1到StringWrapper2构建一个函数 funcWrapper :: StringWrapper1 -> StringWrapper2 一方面,我们想明确表示,我们传递给这个函数的是一个

StringWrapper1
StringWrapper2
成为两种类型的字符串(即
newtype-StringWrapper1=StringWrapper1-string
newtype-StringWrapper2=StringWrapper2

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

funcWrapper :: StringWrapper1 -> StringWrapper2
一方面,我们想明确表示,我们传递给这个函数的是一个
StringWrapper1
,因此我们不想仅仅将
StringWrapper1
视为
String
的类型同义词(我自己的经验可以证明,这会导致bug)。另一方面,在概念上构建函数时,我们仍然在某种程度上考虑
String
s。然后我们要做的是首先构建
func
,它不需要我们不断地包装和展开类型:

func :: String -> String
然后,我们使用
func
构建
funcWrapper

funcWrapper :: StringWrapper1 -> StringWrapper2
funcWrapper (StringWrapper1 str) = StringWrapper2 (func str)

问题:这是惯用语吗?经常用
func
funcWrapper
复制每个函数似乎很尴尬。Haskell是否提供了我所缺少的其他方法?或者我应该只使用类型同义词吗?

为什么不编写一个可以包装其他函数的函数呢

wrap :: (String -> String) -> StringWrapper1 -> StringWrapper2
wrap f (StringWrapper1 str) = StringWrapper2 (f str)

这会将任何
String->String
提升到
StringWrapper1->StringWrapper2
首先,您应该考虑leftaroundabout的注释,并确保newytpes实际上是有意义的。这就是说,这种包装和拆开包装确实是日常用品,但你可以让它更方便。一种方法是利用字符串包装器是单态函子(而不是多态的
函子
s),因此您可以编写映射函数,例如:

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可遍历包中的类

在两个包装器之间定义转换函数也很容易(用花哨的行话来说,我们会说这是两个函子之间的自然转换):

rewrap1as2
可以从
Data.concure
简单地实现为
concure
。有关详细信息,请参阅。)

然后,可以根据以下更基本的函数定义中的
wrap

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

如果你想要更不冗长的东西,你可能会喜欢这个软件包,或者。然而,这可能值得一个单独的答案。

正如其他人所说,你应该确保这是你真正想要做的事情(参见leftaroundabout的评论)。如果是,则可以使用在具有相同运行时表示形式的类型之间进行转换:

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

...

funcWrapper :: StringWrapper1 -> StringWrapper2
funcWrapper = coerce func
使用该包,可以编写如下内容

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

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

这很优雅;但是,您仍然需要为每种函数类型创建这些包装函数。如果
StringWrapper1
被嵌入到记录数据结构中呢?似乎仍然有很多重复。但最后:你文章中的解决方案是惯用的Haskell方法吗?@George注意到,“创建这些包装函数”相当于使用
wrap f
,而我本来会使用
f
。无需为所有此类函数提供具有自己标识符的定义。我只需要定义普通的
f
,并让调用者在需要时使用
wrap f
。没有重复。如果您在概念上考虑字符串,那么为什么需要类型级别的区别?另一种类型的要点是,即使实际实现是一个字符串,您也会考虑一些概念上不同的东西。我认为
generalizednewtypedering
通常比使用
强制
之类的方法(这里的答案似乎重点关注)更好,但这要视情况而定。这似乎是最优雅的。是否不赞成使用它?@George不,如果这是您真正想要做的,我从来没有见过它不赞成(通常您希望一个新类型在某个时候在外部是“不透明的”。通常在其定义的模块之外)<代码>强制的开销也为零(包装/展开解决方案的情况并非如此),因为它在编译过程中被消除。请注意,
强制
不安全
有很大不同<代码>强制总是安全的。包装和解包解决方案的开销也为零。@BenjaminHodgson啊,没错。如果您有类似于
map(\(StringWrapper1str)->str)包装器的东西,那么开销差异就会发挥作用。
{-# 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