为什么我必须用 mapM 组合 id

Why do I have to compose id with mapM

我有以下方法:

nucleotideComplement :: Char -> Either String Char
nucleotideComplement 'G' = Right 'C'
nucleotideComplement 'C' = Right 'G'
nucleotideComplement 'T' = Right 'A'
nucleotideComplement 'A' = Right 'U'
nucleotideComplement x = Left "Not a valid RNA nucleotide."

并想定义另一个:

toRNA :: String -> String
toRNA = either error mapM nucleotideComplement

但是我在这里遇到类型错误。然而,这样做似乎可以解决问题:

toRNA :: String -> String
toRNA = either error id . mapM nucleotideComplement

我不明白为什么会这样

首先,id 的类型为 a -> a。接下来获取mapM nucleotideComplementid . mapM nucleotideComplement的类型(:t)时,好像是一样的。为什么我得到的效果如此不同?

希望有人能进一步澄清这一点。

我认为你读错了...

either error id . mapM nucleotideComplement

你似乎认为这意味着

either error (id . mapM nucleotideComplement)

实际上它的意思是

(either error id) . (mapM nucleotideComplement)

你没有在任何地方做 id . mapM nucleotideComplement。您正在应用 mapM,然后将结果传递给 either,这将应用 errorid,具体取决于它看到的是 Left 还是 Right.

其中一个的类型是 (a -> c) -> (b -> c) -> Either a b -> c。所以你将它应用到 error,你会得到 (b -> c) -> Either String b -> c,然后你将它应用到 mapM,你会得到 Monad m => Either String (a -> m b) -> [a] -> m [b]。然后你将它应用到 nucleotideComplement 并且你得到一个错误,因为 nucleotideComplement 是一个函数而不是 Either.

换句话说,当您打算用两个参数调用它时,您将 either 应用于三个参数,其中第二个参数是将 mapM 应用于 nucleotideComplement 的结果。要使用您想要的参数调用该函数,您可以编写 either error (mapM nucleotideComponent),但这仍然不起作用,因为 either 的第二个参数应该是一个接受 Char 的函数(因为您有一个 Either String Char),而不是一个接受 monad 的人。要实现您想要的效果,您可以编写 either error nucleotideComponent 或使用 .,正如您已经发现的那样。

带有 . 的版本有效,因为 Haskell 的优先规则表明 either error id . mapM nucleotideComplement 等同于 (either error id) . (mapM nucleotideComplement),而不是 (either error id . mapM) nucleotideComplementeither error (id . mapM nucleotideComplement). either error id 是将 Either String b 转换为 Either a b 的函数,其中左边的大小写会导致错误,而 mapM nucleotideComplement 是将 m Char 转换为另一个的函数m Char 对于任何 monad m,char 是 "flipped" - 在这种情况下 mEither String。因此,通过组合这两个函数,您将得到一个将 Either String Char 转换为 Either a Char 的函数,其中右侧的情况是翻转的字符,左侧的情况导致错误。

当然 either error flipNucleotide 是更简单的解决方案。

这并不能完全解决您的类型错误,但我想建议您重新考虑您的陈述。具体来说,通常最好使用类型来强制执行不变量,避免可能抛出错误或异常的部分函数,​​并避免不小心混淆可能属于代码不同部分的相关事物。有多种方法可以解决这个问题,但这里有一个。这种方法假设 DNA 和 RNA 具有完全不同种类的核苷酸。从化学上讲,这不是真的,但它可能是对您正在做的事情的合理表示。实际上对化学现实进行编码可能超出了 Haskell 的类型系统的能力,并且在这种情况下对于捕获错误可能实际上用处不大。

data DNANucleotide = GD | CD | TD | AD
data RNANucleotide = GR | CR | UR | AR

toStringDNA :: [DNANucleotide] -> String
toStringDNA = map (\nucleotide -> case nucleotide of
  {GD -> 'G'; CD -> 'C'; TD -> 'T'; AD -> 'A'})

toStringRNA = ...

fromCharDNA :: Char -> Maybe DNANucleotide
fromCharDNA 'G' = Just GD
fromCharDNA 'C' = Just CD
...
fromCharDNA _ = Nothing

fromCharRNA = ...

fromStringDNA :: String -> Maybe [DNANucleotide]
fromStringDNA = mapM fromCharDNA

fromStringRNA :: String -> Maybe [RNANucleotide]
fromStringRNA = mapM fromCharRNA

一旦您了解了 使用 DNA 和 RNA 的实际机制,而不是从字符串中读取它们,就不会再有错误了:

transcribeN :: DNANucleotide -> RNANucleotide
transcribeN GD = CR
transcribeN CD = GR
transcribeN TD = AR
transcribeN AD = UR

transcribe :: [DNANucleotide] -> [RNANucleotide]
transcribe = map transcribeN