【问题标题】:Haskell scope of a functionHaskell 函数的作用域
【发布时间】:2020-12-23 23:10:57
【问题描述】:

我正在尝试做一些事情,它将作为在“where”子句之后定义的输入函数并输出由它组成的字符串。提到它会将“where”子句之后列出的函数范围限定为 writeOut 函数。 我想带什么(它不工作):

writeOut :: [Char] -> [Char]
writeOut x = x
 where
 number :: Char -> [Char]
 number n = [n]
 multiplication :: [Char] -> [Char] -> [Char]
 multiplication a b = a++"*"++b
 addition :: [Char] -> [Char] -> [Char]
 addition a b = "("++a++"+"++b++")"
 subtraction :: [Char] -> [Char] -> [Char]
 subtraction a b = "("++a++"-"++b++")"
 ...

因此,例如,对于输入:

writeOut multiplication (addition (number '1') (subtraction (number '2') (number '3'))) (number '3')

会给我一个输出:

(1+(2-3))*3

当我删除 writeOut 函数并只保留 'where' 关键字之后的内容时它工作正常,但我真的想在 'writeOut' 函数下'锁定它'。

你可能会问我为什么要这样做,因为我想要另一个函数来为我计算表达式:

writeOut multiplication (addition (number '1') (subtraction (number '2') (number '3'))) (number '3')

Output: (1+(2-3))*3

calc multiplication (addition (number '1') (subtraction (number '2') (number '3'))) (number '3')

Ouput: 0

以防万一你想看看什么是“正常”工作:

number :: Char -> [Char]
number n = [n]
multiplication :: [Char] -> [Char] -> [Char]
multiplication a b = a++"*"++b
addition :: [Char] -> [Char] -> [Char]
addition a b = "("++a++"+"++b++")"
subtraction :: [Char] -> [Char] -> [Char]
subtraction a b = "("++a++"-"++b++")"

【问题讨论】:

  • 你的意思是什么“但是我真的想在'writeOut'功能下'锁定'。”where中的所有内容都是本地范围的,所以这意味着number等在writeOut函数之外是不可访问的。
  • 如果您的目标是允许将函数用作writeOut 的参数,答案是:您不能,也不应该。故意限制用户是没有意义的,特别是如果这些功能有其他合法用途。 Haskell 也不允许您这样做,因为参数都应该是可以在调用函数之外获得的值。
  • 对不起,如果我没有表达清楚我的想法,但是是的,我试图只允许函数用作writeOut的参数,然后是函数@987654332 @我想重新定义它们。我认为目的在下面的第二部分中得到了很好的展示-我想调用所需的输出和功能来获取它们。我知道在 OOP 中这很容易,但这是我第一次接触函数式编程,如果我应该采用另一种方法,很高兴知道。
  • @Lidbey:不要这样做。您确实可以使用类型系统来做类似的事情。例如,在模块中定义 number :: Char -> Foo [Char] 等的东西,因此 Foo 是模块用户无法访问的类型,除非您使用 writeOut,但问题是这里的用途是什么。
  • This 帖子可能对您有所帮助。

标签: haskell


【解决方案1】:

您似乎正在尝试实现 DSL 和该 DSL 的评估器。与其使用一组函数,不如定义一个数据类型,然后为该类型编写一个求值器。

data Expr = Number Int
          | Mult Expr Expr
          | Add Expr Expr
          | Subtract Expr Expr


writeOut :: Expr -> String
writeOut (Number x) = show x
writeOut (Add x y) = "(" ++  writeOut x ++ " + " ++ writeOut y ++ ")"
writeOut (Subtract x y) = "(" ++  writeOut x ++ " - " ++ writeOut y ++ ")"
writeOut (Mult x y) = "(" ++ writeOut x ++ " * " ++ writeOut y ++ ")"


print $ writeOut (Mult (Add (Number 1) (Subtract (Number 2) (Number 3))) (Number 3)

我把它作为一个练习来消除最外面的括号。

作为奖励分配,定义一个不同的评估器,将表达式评估为单个 Int,而不是显示它:

evaluate :: Expr -> Int
evaluate = undefined

【讨论】:

    【解决方案2】:

    您可以利用类型系统来防止人们访问包装在数据构造函数中的内容。例如:

    module MyModule(
        Locked
      , number, multiplication, addition, subtraction
      , writeOut
      ) where
    
    data Locked a = Locked a
    
    writeOut :: Locked a -> a
    writeOut (Locked a) = a
    
    number :: Char -> Locked [Char]
    number n = Locked [n]
    
    multiplication :: Locked [Char] -> Locked [Char] -> Locked [Char]
    multiplication (Locked a) (Locked b) = Locked (a++"*"++b)
    
    addition :: Locked [Char] -> Locked [Char] -> Locked [Char]
    addition (Locked a) (Locked b) = Locked ("("++a++"+"++b++")")
    
    subtraction :: Locked [Char] -> Locked [Char] -> Locked [Char]
    subtraction (Locked a) (Locked b) = Locked ("("++a++"-"++b++")")

    因此这里的模块导出Locked 数据构造函数,因此尽管您可以在任何地方使用numbermultiplication等,但您如果不最终使用writeOut,则无法访问包装在Locked 数据构造函数中的结果。

    使用Locked 的另一个好处是人们不能使用任意的String:确实,这里的number 只允许直接引入单个字符,因此不能连接两个任意字符。因此Locked 可以防止pople 生成13 之类的东西,因为number '1'number '3' 只能与additionsubtraction 等结合使用。

    但是人们仍然可以在任何地方使用这些功能,只是如果不最终使用writeOut,它将非常无用。

    【讨论】:

    • 我正在尝试实现这个解决方案,它有一些问题,因为它需要一些调整,比如Locked,在我结束之前,有人已经提供了一个更清晰的解决方案,谢谢反正你的参与! :)
    • @Libdey:谢谢。通常它现在应该被修复了。
    【解决方案3】:

    目前,您对numbermultiplicationaddition 等的定义都是对字符串或字符进行操作的函数。因此,即使假设它们是在顶层定义的,您也可以在如下表达式中引用它们:

    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 函数计算表达式的 ,则需要在某处对其进行概括。

    有两种典型的方法可以做到这一点。

    一种方法是创建一个 数据类型 来表示表达式,然后您的两个函数 writeOutcalc 将使用此类型的值并分别生成一个字符串或数字:

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

    请注意,writeOutcalc 的参数周围仍然需要括号;如果你写了writeOut Plus (Number 1) (Number 2),那就意味着用三个参数调用writeOutPlusNumber 1Number 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(或其他数字类型,如IntegerRationalDouble) , 将表达式解释为值。

    您可以通过创建一个类型类来表达这一点,该类型类的方法是您的表达式中可能出现的术语:

    -- 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 通常默认使用的方法,除非他们有特定的理由不这样做。

    如果您想将表达式的构造和解释限制在这些特定函数中,您可以导出additionmultiplication 等。来自模块的函数,并将数据类型保留为私有,那么构造 writeOutcalc 的参数的唯一方法是调用这些函数:

    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 = …
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2017-05-26
      • 1970-01-01
      • 2015-08-22
      • 2021-05-22
      • 2012-03-07
      • 1970-01-01
      相关资源
      最近更新 更多