我正在学习Haskell,作为练习,我正在尝试将代码后面的read_from函数转换为Haskell。取自Peter Norvig的Scheme翻译。 有这么简单的方法吗?
def read(s):
"Read a Scheme expression from a string."
return read_from(tokenize(s))
parse = read
def tokenize(s):
"Convert a string into a list of tokens."
return s.replace('(',' ( ').replace(')',' ) ').split()
def read_from(tokens):
"Read an expression from a sequence of tokens."
if len(tokens) == 0:
raise SyntaxError('unexpected EOF while reading')
token = tokens.pop(0)
if '(' == token:
L = []
while tokens[0] != ')':
L.append(read_from(tokens))
tokens.pop(0) # pop off ')'
return L
elif ')' == token:
raise SyntaxError('unexpected )')
else:
return atom(token)
def atom(token):
"Numbers become numbers; every other token is a symbol."
try: return int(token)
except ValueError:
try: return float(token)
except ValueError:
return Symbol(token)
答案 0 :(得分:53)
有一种直接的方式来音译" Python进入Haskell。这可以通过聪明地使用monad变换器来实现,这听起来很可怕,但事实并非如此。你看,由于纯度的原因,当你想要使用诸如可变状态之类的效果(例如append
和pop
操作正在执行变异)或异常时,你必须让它更明确一些。让我们从顶部开始。
parse :: String -> SchemeExpr
parse s = readFrom (tokenize s)
Python docstring说"从一个字符串"中读取一个Scheme表达式,所以我只是冒昧地将其编码为类型签名(String -> SchemeExpr
)。该文档字符串已过时,因为该类型传达相同的信息。现在...... 是什么SchemeExpr
?根据您的代码,scheme表达式可以是int,float,symbol或scheme表达式列表。让我们创建一个代表这些选项的数据类型。
data SchemeExpr
= SInt Int
| SFloat Float
| SSymbol String
| SList [SchemeExpr]
deriving (Eq, Show)
为了告诉Haskell我们正在处理的Int
应该被视为SchemeExpr
,我们需要用SInt
标记它。与其他可能性一样。让我们继续tokenize
。
tokenize :: String -> [Token]
同样,docstring变成了一个类型签名:将String
转换为Token
的列表。好吧,什么是令牌?如果您查看代码,您会注意到左侧和右侧的paren字符显然是特殊的令牌,它表示特定的行为。还有别的......非特别的。虽然我们可以创建一种数据类型,以便更清楚地将parens与其他令牌区分开来,但我们只需使用Strings,就可以更接近原始的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
。这是应该做的诀窍:
-- add imports to top of file
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
的文档,这个简单的算法应该是完全合理的。 Haskellers通常将其写为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-esque函数调用链,你只需要放置" 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
。首先,问自己一个问题:这个功能有什么副作用?它通过tokens
使其参数pop
变异,并且在名为L
的列表中有内部变异。它还引发了SyntaxError
异常。在这一点上,大多数Haskellers会举起手来说"哦,不!副作用! !毛"但事实是,Haskellers也一直使用副作用。我们只是称他们为" monads"为了吓跑人们,不惜一切代价避免成功。可以使用State
monad完成变异,使用Either
monad进行异常(惊喜!)。我们希望同时使用两者,因此我们实际上使用" monad变换器",我将稍微解释一下。一旦你学会了解过这个问题,它就不会可怕了。
首先,一些实用程序。这些只是一些简单的管道操作。 raise
会让我们提出例外情况"就像在Python中一样,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
或{{1} }。这让人想起如何在Java中注释方法将引发哪些异常。请注意,SyntaxError
的类型签名也必须更改,因为它可能会引发SyntaxError。
parse
我们将对传入的令牌列表执行有状态计算。但是,与Python不同,我们不会对调用者粗鲁并且改变传递给我们的列表。相反,我们将建立自己的状态空间并将其初始化为我们给出的令牌列表。我们将使用data SyntaxError = SyntaxError String
deriving (Show)
parse :: String -> Either SyntaxError SchemeExpr
readFrom :: [Token] -> Either SyntaxError SchemeExpr
readFrom = evalStateT readFrom'
符号,它提供语法糖,使其看起来像我们必须编程。 do
monad转换器为我们提供了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' -- here we overwrite the state with the "rest" of the tokens
case token of
"(" -> (SList . reverse) `fmap` execStateT readWithList []
")" -> raise (SyntaxError "unexpected close paren")
_ -> return (atom token)
部分分解为单独的代码块,
因为我希望你看到类型签名。这部分代码介绍
新范围,因此我们只需将另一个readWithList
分层放在monad堆栈之上
我们以前有过。现在,StateT
,get
和put
操作会引用
在Python代码中称为modify
的东西。如果我们想要执行这些操作
在L
上,我们可以简单地按顺序为tokens
添加操作
去除monad堆栈的一层。
lift
在Haskell中,附加到列表的末尾是低效的,所以我改为先行,然后在之后颠倒列表。如果您对性能感兴趣,那么可以使用更好的类似列表的数据结构。
以下是完整文件:http://hpaste.org/77852
所以,如果你是Haskell的新手,那么这可能看起来很恐怖。我的建议是给它一些时间。 Monad的抽象并不像人们想象的那样可怕。您只需要了解大多数语言已经出现的内容(变异,异常等),而Haskell则通过库提供。在Haskell中,您必须明确指定所需的效果,并且控制这些效果不太方便。然而,作为交换,Haskell提供了更多的安全性,因此您不会意外地混淆错误的效果和更多的力量,因为您可以完全控制如何组合和重构效果。
答案 1 :(得分:12)
在Haskell中,您不会使用改变其操作数据的算法。所以不,没有直接的方法可以做到这一点。但是,可以使用递归来重写代码以避免更新变量。下面的解决方案使用MissingH包,因为Haskell恼人地没有一个适用于字符串的replace
函数。
import Data.String.Utils (replace)
import Data.Tree
import System.Environment (getArgs)
data Atom = Sym String | NInt Int | NDouble Double | Para deriving (Eq, Show)
type ParserStack = (Tree Atom, Tree Atom)
tokenize = words . replace "(" " ( " . replace ")" " ) "
atom :: String -> Atom
atom tok =
case reads tok :: [(Int, String)] of
[(int, _)] -> NInt int
_ -> case reads tok :: [(Double, String)] of
[(dbl, _)] -> NDouble dbl
_ -> Sym tok
empty = Node $ Sym "dummy"
para = Node Para
parseToken (Node _ stack, Node _ out) "(" =
(empty $ stack ++ [empty out], empty [])
parseToken (Node _ stack, Node _ out) ")" =
(empty $ init stack, empty $ (subForest (last stack)) ++ [para out])
parseToken (stack, Node _ out) tok =
(stack, empty $ out ++ [Node (atom tok) []])
main = do
(file:_) <- getArgs
contents <- readFile file
let tokens = tokenize contents
parseStack = foldl parseToken (empty [], empty []) tokens
schemeTree = head $ subForest $ snd parseStack
putStrLn $ drawTree $ fmap show schemeTree
foldl
是haskeller的基本结构化递归工具,它的作用与while循环和递归调用read_from
相同。我认为代码可以改进很多,但我不习惯Haskell。下面几乎直接将上述音译转换为Python:
from pprint import pprint
from sys import argv
def atom(tok):
try:
return 'int', int(tok)
except ValueError:
try:
return 'float', float(tok)
except ValueError:
return 'sym', tok
def tokenize(s):
return s.replace('(',' ( ').replace(')',' ) ').split()
def handle_tok((stack, out), tok):
if tok == '(':
return stack + [out], []
if tok == ')':
return stack[:-1], stack[-1] + [out]
return stack, out + [atom(tok)]
if __name__ == '__main__':
tokens = tokenize(open(argv[1]).read())
tree = reduce(handle_tok, tokens, ([], []))[1][0]
pprint(tree)