【问题标题】:How to translate this python to Haskell?如何将此python翻译成Haskell?
【发布时间】:2012-11-17 15:10:05
【问题描述】:

我正在学习 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)

【问题讨论】:

  • 如果你想进一步推进这个项目,你可能想看看Write Yourself a Scheme in 24 Hours
  • +1 我一直在寻找一个很好的借口来写博客。我想我会把它写成一个 SO 答案。
  • 正如 AndrewC 所建议的那样,我认为在 Haskell 中使用解析器组合器执行此操作更为自然。我强烈建议通读该教程。 Monad 转换器很棒,但在 Haskell 中没有必要这样做。

标签: haskell


【解决方案1】:

有一种直接的方法可以将 Python “音译”成 Haskell。这可以通过巧妙地使用 monad 转换器来完成,这听起来很吓人,但事实并非如此。您会看到,由于纯度,在 Haskell 中,当您想要使用可变状态(例如 appendpop 操作正在执行突变)或异常等效果时,您必须使其更加明确。让我们从顶部开始。

parse :: String -> SchemeExpr
parse s = readFrom (tokenize s)

Python 文档字符串说“从字符串中读取 Scheme 表达式”,所以我只是冒昧地将其编码为类型签名 (String -> SchemeExpr)。该文档字符串已过时,因为该类型传达了相同的信息。现在... SchemeExpr 是什么?根据您的代码,方案表达式可以是 int、float、符号或方案表达式列表。让我们创建一个表示这些选项的数据类型。

data SchemeExpr
  = SInt    Int
  | SFloat  Float
  | SSymbol String
  | SList   [SchemeExpr]
  deriving (Eq, Show)

为了告诉 Haskell 我们正在处理的Int 应该被视为SchemeExpr,我们需要用SInt 标记它。其他可能性也是如此。让我们继续tokenize

tokenize :: String -> [Token]

同样,文档字符串变成了类型签名:将String 变成Tokens 的列表。那么,什么是令牌?如果查看代码,您会注意到左右括号字符显然是特殊标记,表示特定行为。其他任何东西都是……​​不特别的。虽然我们可以创建一种数据类型来更清楚地区分括号和其他标记,但让我们只使用字符串,以更接近原始 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

如果您阅读了splitOnintercalate 的文档,那么这个简单的算法应该很有意义。 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 没有自动强制转换函数 intfloat,因此我们将构建 tryReadInto。它是这样工作的:我们将线程Either 值。 Either 值是 LeftRight。按照惯例,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 代码中的 ValueExceptions总是被捕获在函数本身中,我们知道 atom 永远不会引发异常。同样,在 Haskell 代码中,即使我们在函数内部使用了Either,我们暴露的接口也是纯的:Token -> SchemeExpr,没有外在可见的副作用。


好的,让我们继续 read_from。首先,问自己一个问题:这个功能有什么副作用?它通过pop 改变它的参数tokens,并且它在名为L 的列表上有内部突变。它还会引发SyntaxError 异常。在这一点上,大多数Haskellers会举手说“哦,不!副作用!恶心!”但事实是,Haskellers 也一直在使用副作用。我们只是称它们为“单子”,以吓跑人们并不惜一切代价避免成功。可以使用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,因此我们将在类型签名中指出结果将是要么SchemeExprSyntaxError。这让人想起在 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 monad 转换器为我们提供了 getputmodify 状态操作。

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 我们以前有的。现在,getputmodify 操作参考 到 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) -- this seems to be missing from the Python

在 Haskell 中,追加到列表的末尾是低效的,所以我改为预先添加,然后在之后反转列表。如果您对性能感兴趣,那么您可以使用更好的类似列表的数据结构。

这是完整的文件:http://hpaste.org/77852


因此,如果您是 Haskell 的新手,那么这可能看起来很可怕。我的建议是给它一些时间。 Monad 抽象并不像人们想象的那么可怕。您只需要了解大多数语言都包含的内容(突变、异常等),Haskell 反而通过库提供。在 Haskell 中,您必须明确指定您想要的效果,并且控制这些效果不太方便。然而,作为交换,Haskell 提供了更高的安全性,因此您不会意外混淆错误的效果,并且功能更强大,因为您可以完全控制如何组合和重构效果。

【讨论】:

  • 如果你打算混合使用EitherState monad,这基本上是Parser monad 的一种变体:newtype Parser a = Parser { runParser :: String -&gt; Either ErrorMessage (String, a) },它完全等同于StateT String (Either ErrorMessage) a。而且,它完全符合他正在做的事情:解析。如果您只是为此定义一个 monad 实例,那么您不需要引入 monad 转换器。
  • 或者,您也可以newtype Parser a = Parser { runParser :: StateT String (Either ErrorMessage) a } deriving (Monad) 并免费获得所有机器。
  • 我想在这里强调的是,Haskell 提供了常用的命令式语言功能作为库。想要突变?使用状态。想要例外?使用任一。想要重新反转控制?使用续。新手应该学习如何识别和使用适当的工具,所以我觉得最好分别说明每一个。
  • 对于如何在惯用的 Haskell 中实现此功能的初学者来说,这是很棒的代码和糟糕的建议。 :D
  • @HeinrichApfelmus 确实,我的目标不是惯用的 Haskell。相反,它(尽可能在 Haskell 中)是 Pythonic。
【解决方案2】:

在 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)

【讨论】:

  • 谢谢,这正是我想要的。