有一种直接的方法可以将Python“音译”成Haskell。这可以通过巧妙地使用单子变换器来实现,听起来很可怕,但其实并不是。你知道,在Haskell中,由于纯度(purity)的原因,当你想要使用像可变状态(例如
append
和
pop
操作正在执行的突变)或异常等效果时,你必须使它更加明确。让我们从顶部开始。
parse :: String -> SchemeExpr
parse s = readFrom (tokenize s)
Python的文档字符串说“从字符串中读取Scheme表达式”,所以我就擅自将其编码为类型签名(String -> SchemeExpr
)。该文档字符串变得过时,因为类型传递了相同的信息。那么,什么是SchemeExpr
?根据您的代码,Scheme表达式可以是int、float、符号或Scheme表达式列表。让我们创建一个表示这些选项的数据类型。
data SchemeExpr
= SInt Int
| SFloat Float
| SSymbol String
| SList [SchemeExpr]
deriving (Eq, Show)
为了告诉 Haskell 我们正在处理的 Int
应该被视为一个 SchemeExpr
,我们需要用 SInt
给它打上标签。其他情况也是如此。现在让我们继续进行 tokenize
。
tokenize :: String -> [Token]
再次强调,文档字符串将变为类型签名:将String
转换为Token
列表。那么,什么是Token?如果你查看代码,你会注意到左右圆括号字符显然是特殊的标记,它们表示特定的行为。其他任何东西都不是... 特殊的。虽然我们可以创建一个数据类型来更清楚地区分圆括号与其他标记,但让我们只使用字符串,以更接近原始的Python代码。
type Token = String
现在让我们尝试编写tokenize
。首先,让我们写一个快捷的运算符,使函数链接看起来更像Python。在Haskell中,您可以定义自己的运算符。
(|>) :: a -> (a -> b) -> b
x |> f = f x
tokenize s = s |> replace "(" " ( "
|> replace ")" " ) "
|> words
words
是 Haskell 中的 split
版本。然而,就我所知,Haskell 没有预先制作好的 replace
版本。这里有一个可以解决问题的版本:
import Data.List.Split (splitOn)
import Data.List (intercalate)
replace :: String -> String -> String -> String
replace old new s = s |> splitOn old
|> intercalate new
如果您阅读了有关
splitOn
和
intercalate
的文档,这个简单的算法应该很清晰。Haskeller通常会将其写成
replace old new = intercalate new . splitOn old
,但我在这里使用
|>
是为了更容易让Python受众理解。
请注意,
replace
需要三个参数,但是以上代码我只用了两个参数调用了它。在Haskell中,您可以部分地应用任何函数,这非常方便。
|>
的工作方式有点像UNIX管道,如果您还不清楚,并且具有更多的类型安全性。
还跟上吗?我们来看看
atom
。那个嵌套的逻辑有点丑陋,所以让我们尝试稍微不同的方法来整理一下。我们将使用
Either
类型来呈现,使代码更加美观。
atom :: Token -> SchemeExpr
atom s = Left s |> tryReadInto SInt
|> tryReadInto SFloat
|> orElse (SSymbol s)
Haskell没有像int
和float
这样的自动转换函数,因此我们将构建一个tryReadInto
函数。下面是它的工作原理:我们将在Either
值之间传递。 Either
值可以是Left
或Right
。通常,Left
用于表示错误或失败,而Right
表示成功或完成。在Haskell中,为了模拟类似Python的函数调用链,只需将“self”参数放在最后一个位置即可。
tryReadInto :: Read a => (a -> b) -> Either String b -> Either String b
tryReadInto f (Right x) = Right x
tryReadInto f (Left s) = case readMay s of
Just x -> Right (f x)
Nothing -> Left s
orElse :: a -> Either err a -> a
orElse a (Left _) = a
orElse _ (Right a) = a
tryReadInto
依赖于类型推断来确定它正在尝试将字符串解析为哪种类型。如果解析失败,它只会在
Left
位置中复制相同的字符串。如果解析成功,则执行所需的任何函数,并将结果放置在
Right
位置。通过提供在前面计算失败的情况下使用的值,
orElse
允许我们消除
Either
。你能看到
Either
如何在此处充当异常的替代品吗?由于 Python 代码中的
ValueException
总是在函数内部被捕获,因此我们知道
atom
永远不会引发异常。同样,在 Haskell 代码中,即使我们在函数内部使用了
Either
,我们公开的接口也是纯的:
Token -> SchemeExpr
,没有外部可见的副作用。
现在,让我们移动到
read_from
。首先,问自己一个问题:这个函数有哪些副作用?它通过
pop
改变其参数
tokens
,并对名为
L
的列表进行内部修改。它还引发了
SyntaxError
异常。此时,大多数 Haskeller 都会举起双手说“哦不!副作用!恶心!”但事实是,Haskeller 也经常使用副作用。我们只是称它们为“单子”以吓唬人们,并避免一切代价获得成功。使用
State
单子可以完成变异,使用
Either
单子(惊喜!)可以处理异常。我们将同时使用两者,因此实际上将使用“单子转换器”,稍后我会解释。一旦学会看过杂乱无章的部分,这并不可怕。
首先是一些工具函数。这些只是一些简单的管道操作。如 Python 中所示,
raise
允许我们“抛出异常”,而
whileM
则允许我们像在 Python 中一样编写 while 循环。对于后者,我们只需要明确指出产生条件效果的顺序:首先执行计算条件的效果,然后如果它是 True,则执行主体的效果并再次循环。
import Control.Monad.Trans.State
import Control.Monad.Trans.Class (lift)
raise = lift . Left
whileM :: Monad m => m Bool -> m () -> m ()
whileM mb m = do
b <- mb
if b
then m >> whileM mb m
else return ()
我们希望再次暴露出一个纯接口。不过,有可能会出现 SyntaxError
,因此我们将在类型签名中指示结果将是要么SchemeExpr
要么 SyntaxError
。这让人想起了 Java 中如何注释方法会引发哪些异常。请注意,parse
的类型签名也必须更改,因为它可能会引发 SyntaxError。
data SyntaxError = SyntaxError String
deriving (Show)
parse :: String -> Either SyntaxError SchemeExpr
readFrom :: [Token] -> Either SyntaxError SchemeExpr
readFrom = evalStateT readFrom'
我们将对传入的令牌列表执行有状态计算。与Python不同的是,我们不会对调用者粗鲁,并改变传递给我们的列表。相反,我们将建立自己的状态空间,并将其初始化为给定的令牌列表。我们将使用 do
符号表示,它提供了语法糖,使它看起来像我们在编写命令式代码。 StateT
单子变换器为我们提供了 get
、put
和 modify
状态操作。
readFrom' :: StateT [Token] (Either SyntaxError) SchemeExpr
readFrom' = do
tokens <- get
case tokens of
[] -> raise (SyntaxError "unexpected EOF while reading")
(token:tokens') -> do
put tokens'
case token of
"(" -> (SList . reverse) `fmap` execStateT readWithList []
")" -> raise (SyntaxError "unexpected close paren")
_ -> return (atom token)
我已将readWithList
部分拆分为一个单独的代码块,因为我希望您看到类型签名。这部分代码引入了一个新作用域,
因此我们只需在之前的monad堆栈上再添加一个StateT
。现在,get
、put
和modify
操作
都引用了Python代码中称为L
的对象。如果我们想对tokens
执行这些操作,那么我们可以简单地在操作前加上lift
以剥离一层monad堆栈。
readWithList :: StateT [SchemeExpr] (StateT [Token] (Either SyntaxError)) ()
readWithList = do
whileM ((\toks -> toks !! 0 /= ")") `fmap` lift get) $ do
innerExpr <- lift readFrom'
modify (innerExpr:)
lift $ modify (drop 1)
在Haskell中,在列表末尾添加元素效率较低,因此我改为在前面添加,然后反转列表。如果您关心性能,那么有更好的类似于列表的数据结构可供使用。
这是完整的文件:
http://hpaste.org/77852
所以,如果您是Haskell的新手,那么这可能看起来很可怕。我的建议是给它一点时间。Monad抽象并不像人们想象的那么可怕。你只需要学会大多数语言内置的东西(变异、异常等)是如何在Haskell中通过库提供的。在Haskell中,您必须明确指定要使用哪些效果,并且控制这些效果不太方便。然而,作为交换,Haskell提供了更多的安全性,以防止您意外混淆错误的效果,并且提供了更多的功能,因为您完全可以控制如何组合和重构效果。