我一直在搜索互联网几天,试图回答我的问题,我终于承认失败了。
我得到了一个语法:
Dig ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
Int ::= Dig | Dig Int
Var ::= a | b | ... z | A | B | C | ... | Z
Expr ::= Int | - Expr | + Expr Expr | * Expr Expr | Var | let Var = Expr in Expr
我被告知使用这种语法解析,评估和打印表达式
运算符* + -
具有正常含义的位置
具体任务是编写函数parse :: String -> AST
以字符串作为输入,并在输入格式正确时返回抽象语法树(我可以认为是这样)。
我被告知我可能需要一个合适的数据类型,并且该数据类型可能需要从其他一些类派生。
按照示例输出
data AST = Leaf Int | Sum AST AST | Min AST | ...
此外,我应该考虑写一个函数
tokens::String -> [String]
将输入字符串拆分为令牌列表
解析应该用
ast::[String] -> (AST,[String])
其中输入是一个令牌列表,它输出一个AST,并解析子表达式我应该简单地递归使用ast函数。
我还应该创建一个printExpr方法来打印结果
printE: AST -> String
printE(parse "* 5 5")
会产生"5*5"
或"(5*5)"
以及评估表达式的功能
evali :: AST -> Int
我只想指出我可能从哪里开始的正确方向。我一般都对Haskell和FP知之甚少,并试图解决这个任务,我用Java做了一些字符串处理功能,这让我意识到我已经偏离轨道了。
所以在正确的方向上有一个小指针,也许是对AST应该是什么样子的解释
连续第三天仍然没有正在运行的代码,我非常感谢任何帮助我找到解决方案的尝试!
提前谢谢!
修改
我可能不清楚: 我想知道我应该如何阅读并将输入字符串标记为制作AST。
答案 0 :(得分:20)
好的,我们来看看你的语法
Dig ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
Int ::= Dig | Dig Int
Var ::= a | b | ... z | A | B | C | ... | Z
Expr ::= Int | - Expr | + Expr Expr | * Expr Expr | Var | let Var = Expr in Expr
这是一个很好的简单语法,因为你可以从第一个标记告诉它将是什么样的表现。
(如果有更复杂的内容,例如+
介于数字之间,或-
用于减法
以及否定,你需要成功列表技巧,解释如下
Functional Parsers。)
我们有一些示例原始输入:
rawinput = "- 6 + 45 let x = - 5 in * x x"
我从语法中理解的是"(- 6 (+ 45 (let x=-5 in (* x x))))"
,
我假设你把它标记为
tokenised_input' = ["-","6","+","4","5","let","x","=","-","5","in","*","x","x"]
符合语法,但你很可能已经
了tokenised_input = ["-","6","+","45","let","x","=","-","5","in","*","x","x"]
更适合您的样本AST
。我认为在你的语法之后命名你的AST是一个好习惯,
所以我要继续替换
data AST = Leaf Int | Sum AST AST | Min AST | ...
带
data Expr = E_Int Int | E_Neg Expr | E_Sum Expr Expr | E_Prod Expr Expr | E_Var Char
| E_Let {letvar::Char,letequal:: Expr,letin::Expr}
deriving Show
我已经命名了E_Let
的位,以使它更清楚地代表它们。
您可以使用isDigit
添加import Data.Char (isDigit)
来帮助:
expr :: [String] -> (Expr,[String])
expr [] = error "unexpected end of input"
expr (s:ss) | all isDigit s = (E_Int (read s),ss)
| s == "-" = let (e,ss') = expr ss in (E_Neg e,ss')
| s == "+" = (E_Sum e e',ss'') where
(e,ss') = expr ss
(e',ss'') = expr ss'
-- more cases
糟糕!太多让条款掩盖了意义,
我们将为E_Prod
编写相同的代码,对E_Let
编写更差的代码。
让我们解决这个问题吧!
处理这个问题的标准方法是编写一些组合器;
而不是通过我们的定义令人厌倦地线索输入[String]
,定义方法
搞乱解析器的输出(map)并结合使用
多个解析器合为一个(升力)。
首先我们应该定义pmap
,我们自己的map
函数等价物,这样我们才能pmap E_Neg (expr1 ss)
而不是let (e,ss') = expr1 ss in (E_Neg e,ss')
pmap :: (a -> b) -> ([String] -> (a,[String])) -> ([String] -> (b,[String]))
非诺,我甚至都看不懂!我们需要一个类型同义词:
type Parser a = [String] -> (a,[String])
pmap :: (a -> b) -> Parser a -> Parser b
pmap f p = \ss -> let (a,ss') = p ss
in (f a,ss')
但如果我这样做会更好
data Parser a = Par [String] -> (a,[String])
所以我可以做到
instance Functor Parser where
fmap f (Par p) = Par (pmap f p)
我会留下那个让你知道你是否喜欢。
当我们有两个解析器运行时,我们还需要处理这种情况, 我们希望使用函数组合他们的结果。这称为将函数提升为解析器。
liftP2 :: (a -> b -> c) -> Parser a -> Parser b -> Parser c
liftP2 f p1 p2 = \ss0 -> let
(a,ss1) = p1 ss0
(b,ss2) = p2 ss1
in (f a b,ss2)
或者甚至可能是三个解析器:
liftP3 :: (a -> b -> c -> d) -> Parser a -> Parser b -> Parser c -> Parser d
我会让你想到如何做到这一点。
在let语句中,您需要liftP5
来解析let语句的各个部分,
提升忽略"="
和"in"
的函数。你可以做
equals_ :: Parser ()
equals_ [] = error "equals_: expected = but got end of input"
equals_ ("=":ss) = ((),ss)
equals_ (s:ss) = error $ "equals_: expected = but got "++s
还有几个人可以帮忙解决这个问题。
实际上,pmap
也可以被称为liftP1
,但map就是这类事物的传统名称。
现在我们已准备好清理expr
:
expr :: [String] -> (Expr,[String])
expr [] = error "unexpected end of input"
expr (s:ss) | all isDigit s = (E_Int (read s),ss)
| s == "-" = pmap E_Neg expr ss
| s == "+" = liftP2 E_Sum expr expr ss
-- more cases
这一切都很好。真的,没关系。但liftP5
会有点长,感觉很乱。
Applicative Functors是要走的路。 记得我建议重构为
data Parser a = Par [String] -> (a,[String])
所以你可以把它变成Functor
的一个实例?也许你不想,
因为所有你获得的是一个新名称fmap
,用于完美工作pmap
和。{
你必须处理所有那些使你的代码混乱的Par
构造函数。
也许这会让你重新考虑;我们可以import Control.Applicative
,
然后使用data
声明,我们可以
定义<*>
,这意味着then
并使用<$>
代替pmap
,*>
含义
<*>-but-forget-the-result-of-the-left-hand-side
所以你要写
expr (s:ss) | s == "let" = E_Let <$> var *> equals_ <*> expr <*> in_ *> expr
这看起来很像你的语法定义,因此编写第一次有效的代码很容易。
这就是我喜欢编写Parsers的方法。事实上,这就是我喜欢写很多东西的方式。
您只需要定义fmap
,<*>
和pure
,所有这些都是简单的,而不是长期重复liftP3
,liftP4
等。
阅读有关Applicative Functors的内容。他们很棒。
使Parser适用的提示:pure
不会更改列表。
<*>
与liftP2
类似,但该函数不是来自外部,而是来自p1
的输出。
答案 1 :(得分:4)
为了从Haskell本身开始,我推荐Learn You a Haskell for Great Good!,这是一本写得很好,很有趣的指南。 Real World Haskell是另一个经常被推荐的起点。
编辑:解析的更基本的介绍是Functional Parsers。我想要如何用PHilip Wadler的成功列表替换失败。可悲的是,它似乎无法在线获取。
要开始在Haskell中解析,我认为你应该先阅读monadic parsing in Haskell,然后再阅读this wikibook example,然后再阅读parsec guide。
你的语法
Dig ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
Int ::= Dig | Dig Int
Var ::= a | b | ... z | A | B | C | ... | Z
Expr ::= Int | - Expr | + Expr Expr | * Expr Expr | Var | let Var = Expr in Expr
建议了一些抽象数据类型:
data Dig = Dig_0 | Dig_1 | Dig_2 | Dig_3 | Dig_4 | Dig_5 | Dig_6 | Dig_7 | Dig_8 | Dig_9
data Integ = I_Dig Dig | I_DigInt Dig Integ
data Var = Var_a | Var_b | ... Var_z | Var_A | Var_B | Var_C | ... | Var_Z
data Expr = Expr_I Integ
| Expr_Neg Expr
| Expr_Plus Expr Expr
| Expr_Times Expr Expr Var
| Expr_Var Var
| Expr_let Var Expr Expr
这本质上是一个递归定义的语法树,不需要再制作另一个。
对于笨重的Dig_
和Integ_
内容感到抱歉 - 他们必须以大写字母开头。
(就我个人而言,我希望立即将Integ
转换为Int
,这样就可以完成newtype Integ = Integ Int
,并且可能已经完成了newtype Var = Var Char
dig
但这可能不适合你。)
完成基本操作后,var
和neg_
,plus_
,in_
,expr
等等。使用Applicative界面进行构建,例如Expr
的解析器expr = Expr_I <$> integ
<|> Expr_Neg <$> neg_ *> expr
<|> Expr_Plus <$> plus_ *> expr <*> expr
<|> Expr_Times <$> times_ *> expr <*> expr
<|> Expr_Var <$> var
<|> Expr_let <$> let_ *> var <*> equals_ *> expr <*> in_ *> expr
就像
{{1}}
几乎所有的时间,你的Haskell代码都很干净,与你给出的语法非常相似。
答案 2 :(得分:1)
AST
的定义正确,然后尝试实施evali
将是一个良好的开端。
你列出的语法很有趣......你似乎想输入* 5 5
,但输出5*5
,这是一个奇怪的选择。这真的应该是一元减号,而不是二元吗?同样地,* Expr Expr Var
看起来可能是您打算输入* Expr Expr | Var
...
无论如何,对你的意思做出一些假设,你的AST看起来会像这样:
data AST = Leaf Int | Sum AST AST | Minus AST | Var String | Let String AST AST
现在,让我们尝试printE
。它需要一个AST并给我们一个字符串。根据上面的定义,AST必须是五种可能的东西之一。你只需要找出每个人要打印的内容!
printE :: AST -> String
printE (Leaf x ) = show x
printE (Sum x y) = printE x ++ " + " ++ printE y
printE (Minus x ) = "-" ++ printE x
...
show
将Int
变为String
。 ++
将两个字符串连接在一起。我会让你解决剩下的功能。 (棘手的是,如果你想要它打印括号以正确显示子表达式的顺序......因为你的语法没有提到括号,我猜不是。)
现在,evali
怎么样?嗯,这将是一个类似的交易。如果AST是Leaf x
,则x
是Int
,您只需返回该值。如果您有Minus x
,那么x
不是整数,它是AST,因此您需要将其转换为evali
的整数。该函数看起来像
evali :: AST -> Int
evali (Leaf x ) = x
evali (Sum x y) = (evali x) + (evali y)
evali (Minus x ) = 0 - (evali x)
...
到目前为止效果很好。可是等等!看起来您应该能够使用Let
来定义新变量,并稍后使用Var
引用它们。那么,在这种情况下,您需要在某处存储这些变量。这将使功能变得更加复杂。
我的建议是使用Data.Map
来存储变量名称列表及其对应的值。您需要将变量映射添加到类型签名。你可以这样做:
evali :: AST -> Int
evali ast = evaluate Data.Map.empty ast
evaluate :: Map String Int -> AST -> Int
evaluate m ast =
case ast of
...same as before...
Let var ast1 ast2 -> evaluate (Data.Map.insert var (evaluate m ast1)) ast2
Var var -> m ! var
所以evali
现在只需使用空变量映射调用evaluate
。当evaluate
看到Let
时,它会将变量添加到地图中。当它看到Var
时,它会在地图中查找名称。
首先将解析一个字符串转换为AST,这又是一个完整的其他答案......