目前,您对number、multiplication、addition 等的定义都是对字符串或字符进行操作的函数。因此,即使假设它们是在顶层定义的,您也可以在如下表达式中引用它们:
writeOut multiplication (addition (number '1') (subtraction (number '2') (number '3'))) (number '3')
这意味着writeOut必须能够接受函数multiplication作为它的第一个参数,(addition …)的结果作为它的第二个参数,(number …)的结果作为它的第三个参数,所以它需要像([Char] -> [Char] -> [Char]) -> [Char] -> [Char] -> [Char] 这样的类型。重载writeOut 以这种方式工作并允许任何其他表达式作为参数实际上是不可能的;如果你尝试使用类型类来做这件事,你会很快发现它会产生很多不明确的类型,因此需要很多类型注释。
但是有多种更好的方法,具体取决于您在这里真正想要实现的目标。最简单的就是将这些函数移到顶层:
writeOut :: [Char] -> [Char]
writeOut x = x
number :: Char -> [Char]
number n = [n]
multiplication :: [Char] -> [Char] -> [Char]
multiplication a b = a++"*"++b
…
然后像writeOut (multiplication (number '2') (number '3')) 一样调用writeOut,表达式用括号括起来。但是这里writeOut 是完全多余的:它等价于标识函数id,它只是返回未修改的参数。构造字符串的所有实际工作都由其他函数完成。
这就提出了一个要点:这些函数实际上并不是在构造一个表达式,它们是在构造一个字符串,并丢弃有关表达式结构的信息。如果您还希望能够使用 calc 函数计算表达式的 值,则需要在某处对其进行概括。
有两种典型的方法可以做到这一点。
一种方法是创建一个 数据类型 来表示表达式,然后您的两个函数 writeOut 和 calc 将使用此类型的值并分别生成一个字符串或数字:
-- An expression is:
data Expression
-- A literal number, containing an ‘Int’ (or whichever type you need);
= Number Int
-- Or an operation on some subexpressions.
| Addition Expression Expression
| Multiplication Expression Expression
| Subtraction Expression Expression
| …
-- You can add this clause to allow ‘Expression’ to be displayed
-- as Haskell code for debugging purposes in GHCi. Generally you
-- should derive ‘Show’ instead of making your own instance.
--
-- deriving (Show)
writeOut :: Expression -> String
writeOut expression = case expression of
Number n -> show n
Addition a b -> concat ["(", writeOut a, " + ", writeOut b, ")"]
…
calc :: Expression -> Int
calc expression = case expression of
Number n -> n
Addition a b -> calc a + calc b
…
然后您可以构造一个表达式,然后分别将其转换为字符串、数字或您需要的任何其他内容,例如在 GHCi 中:
> :type Number
Number :: Int -> Expression
> :type Multiplication
Multiplication :: Expression -> Expression -> Expression
> example = Multiplication (Addition (Number 1) (Subtraction (Number 2) (Number 3))) (Number 3)
> :type example
example :: Expression
> writeOut example
"((1+(2-3))*3)"
> calc example
0
> calc (Plus (Number 1) (Number 2))
3
请注意,writeOut 或 calc 的参数周围仍然需要括号;如果你写了writeOut Plus (Number 1) (Number 2),那就意味着用三个参数调用writeOut,Plus、Number 1和Number 2,这不是你想要的。
如果您不想使用大写的构造函数名称,您可以创建等效的小写构造函数:
number :: Int -> Expression
number n = Number n
-- Or:
-- number = Number
addition :: Expression -> Expression -> Expression
addition a b = Addition a b
-- Or:
-- addition = Addition
…
第二种技术被称为无标签最终样式。它稍微高级一些,可能不是您想要的,但为了完整起见,我将其包括在内。本质上,您创建了一个类型类,而不是数据类型,它表示作为表达式解释的类型集。在您的情况下,它们是String,表示将表达式解释为漂亮的打印文本(writeOut)和Int(或其他数字类型,如Integer、Rational 或Double) , 将表达式解释为值。
您可以通过创建一个类型类来表达这一点,该类型类的方法是您的表达式中可能出现的术语:
-- The class of interpretations of expressions.
class Interpretation a where
number :: Int -> a
addition :: a -> a -> a
subtraction :: a -> a -> a
multiplication :: a -> a -> a
…
然后是不同解释类型的实例:
-- Place this at the top of the file if you include
-- the type signatures in the definitions below.
{-# LANGUAGE InstanceSigs #-}
instance Interpretation String where
number :: Int -> String
number n = show n
addition :: String -> String -> String
addition a b = concat ["(", a, " + ", b, ")"]
…
instance Interpretation Int where
number :: Int -> Int
number n = n
addition :: Int -> Int -> Int
addition a b = a + b
…
然后像multiplication (addition …) (number 3) 这样的表达式有一个重载类型Interpretation a => a,您可以在不同的具体类型上对其进行评估:
> example :: Interpretation a => a; example = multiplication (addition (number 1) (subtraction (number 2) (number 3))) (number 3)
> example :: String
"((1+(2-3))*3)"
> example :: Int
0
然而,虽然这很灵活,但它也使得引入新操作更加复杂,并且需要一些更高级的类型系统功能才能执行诸如在运行时从字符串解析表达式等操作。
因此,我建议您坚持使用数据类型方法,这是 Haskeller 通常默认使用的方法,除非他们有特定的理由不这样做。
如果您想将表达式的构造和解释限制在这些特定函数中,您可以仅导出addition、multiplication 等。来自模块的函数,并将数据类型保留为私有,那么构造 writeOut 和 calc 的参数的唯一方法是调用这些函数:
module Expr
( number
, addition
, subtraction
…
, writeOut
, calc
) where
-- Expression data type, *not* exported:
data Expression = …
-- Constructor functions, exported:
number :: Int -> Expression
number = Number
…
-- Computations on expressions, also exported:
writeOut :: Expression -> String
writeOut = …
calc :: Expression -> Int
calc = …