如何在 Haskell 中创建函数的字符串表示形式。如何以一种奇特的方式打印一个函数?

How to create a string representation of a function in Haskell. How to print a function in a fancy way?

我的目标是能够将布尔表达式表示为字符串,例如 "True or False is True"。为了让它成为可能,我首先做了一些布尔谓词:

and' :: Bool -> Bool -> Bool
and' p q = p && q

or' :: Bool -> Bool -> Bool
or' p q = p || q

-- ... same for nor, nand, xor, imply and equivalent

equ' :: Bool -> Bool -> Bool
equ' p q = p == q

在那之后,我决定制作一个将函数映射到字符串的函数。我依赖 Haskell 的模式匹配功能,但我的技巧没有奏效。

-- representation function, a.k. "show" for functions
repr :: (Bool -> Bool -> Bool) -> [Char]
repr and'  = "and"
repr or'   = "or"
repr nand' = "nand"
repr nor'  = "nor"
repr xor'  = "xor'"
repr impl' = "implies"
repr equ'  = "equivalent to"
repr other = error "No representation for the given predicate"

GHC认为函数名是参数名,一般情况下只考虑第一个定义。对于剩余的行,GHC 会发出警告,指出“模式匹配是多余的”。这是 运行 repr 函数的示例:

*LogicH99> repr equ'
"and"

预计"equivalent to"

是否可以在 Haskell 中以奇特的方式打印函数?

对于一般的功能,不,没有。但是对于 Bool -> Bool -> Bool 类型的函数来说,可能性太小以至于通过做这样的事情来穷举所有输入是可行的:

repr f = case (f False False, f False True, f True False, f True True) of
    (False, False, False, True) -> "and"
    (False, True, True, True) -> "or"
    -- ...
    (True, False, False, True) -> "equivalent to"
    _ -> error "No representation for the given predicate"

对于您的目标,可能还有另一种方法可以将布尔表达式更改为字符串。下面是一个例子:

toString :: Bool -> Bool -> Bool -> String -> String
toString p q r op = show p ++ op ++ show q ++ " is " ++ show r

and', or' :: Bool -> Bool -> (Bool, String)
and' p q = let r = p && q
            in (r, toString p q r " and ")
or' p q = let r = p || q
           in (r, toString p q r " or ")

所以如果要得到布尔结果,就得到结果元组的第一个元素,如果是字符串表达式,就得到第二个元素。

λ> snd $ and' True False
"True and False is False"

λ> fst $ and' True False
False

您只需测试 4 个输入,这样您就可以通过对输入进行详尽评估来定义模式,如前所述。然后,您可以定义一个仅匹配 (&&):

的模式同义词
{-# LANGUAGE PatternSynonyms, ViewPatterns #-}

isAnd :: (Bool -> Bool -> Bool) -> Bool
isAnd (·) = and
 [ True  · True  == True
 , True  · False == False
 , False · True  == False
 , False · False == False
 ]

-- case (&&) of 
--   IsAnd -> "and"
pattern IsAnd :: Bool -> Bool -> Bool
pattern IsAnd <- (isAnd -> True)
  where IsAnd = (&&)

describe :: (Bool -> Bool -> Bool) -> String
describe IsAnd = "and"
describe ..

这很有趣,但您应该只创建一个数据类型 data Op = And | .. 或通过其签名对其进行索引

type Op :: Type -> Type
data Op sig where
 OpAnd   :: Op (Bool -> Bool -> Bool)
 OpNot   :: Op (Bool -> Bool)
 OpTrue  :: Op Bool
 OpFalse :: Op Bool

在这种情况下,常规的解决方案是引入数据类型(“初始”编码)或类型类(“最终”编码),然后您可以将函数和漂亮打印的形式定义为两种不同的解释。例如,对于普通数据类型,您可以只进行模式匹配:

data Exp
  = Lit Bool
  | And Exp Exp
  | Or Exp Exp
  | Not Exp
  | Equ Exp Exp
  deriving (Read, Show)
  -- Roundtrip to a debugging representation string.
  -- (Plus whatever other standard classes you need.)

-- Evaluate an expression to a Boolean.
eval :: Exp -> Bool
eval (Lit b) = b
eval (And e1 e2) = eval e1 && eval e2
eval (Or e1 e2) = eval e1 || eval e2
eval (Not e) = not (eval e)
eval (Equ e1 e2) = eval e1 == eval e2

-- Render an expression to a pretty-printed string.
render :: Exp -> String
render (Lit b) = show b
render (And e1 e2) = concat ["(", render e1, " and ", render e2, ")"]
render (Or e1 e2) = concat ["(", render e1, " or ", render e2, ")"]
render (Not e) = concat ["not ", render e]
render (Equ e1 e2) = concat ["(", render e1, " equivalent to ", render e2, ")"]

使用 GADT,您可以添加一些更具体的静态类型:

{-# Language GADTs #-}

data Exp t where
  Lit :: Bool -> Exp Bool
  And, Or, Equ :: Exp (Bool -> Bool -> Bool)
  Not :: Exp (Bool -> Bool)
  (:$) :: Exp (a -> b) -> Exp a -> Exp b

eval :: Exp t -> t
eval (Lit b) = b
eval And = (&&)
eval Or = (||)
eval Equ = (==)
eval Not = not
eval (f :$ x) = eval f $ eval x

render :: Exp t -> String
render (Lit b) = show b
render And = "and"
render Or = "or"
render Equ = "equivalent to"
render Not = "not"
render (f :$ x :$ y) = concat [render x, " ", render f, " ", render y]
render (f :$ x) = concat [render f, " ", render x]

或者最后使用类型类,结果类似:

-- The set of types that can be used as
-- /interpretations/ of expressions.
class Exp r where
  lit' :: Bool -> r
  and', or', equ' :: r -> r -> r
  not' :: r -> r

-- Expressions can be interpreted by evaluation.
instance Exp Bool where
  lit' = id
  and' = (&&)
  or' = (||)
  equ' = (==)
  not' = not

-- A pretty-printed string.
newtype Pretty = Pretty String

-- They can also be interpreted by pretty-printing.
instance Exp Pretty where
  lit' b = Pretty $ show b
  and' r1 r2 = Pretty $ concat ["(", r1, " and ", r2, ")"]
  or' r1 r2 = Pretty $ concat ["(", r1, " or ", r2, ")"]
  equ' r1 r2 = Pretty $ concat ["(", r1, " equivalent to ", r2, ")"]
  not' r = Pretty $ concat ["not ", r]

这增加了您在这里可能不需要的灵活性和复杂性,但我提到它是因为这种设计模式可以用于解决更大的问题。 (有关更多信息,请参阅 Tagless-final Style。)

我想补充 Joseph 的回答。它执行相同的计算(即 运行 在最坏情况下所有可能的输入上的给定函数),但可能以更具可读性的方式。它使用 universe and containers 包。

import Data.Maybe
import Data.Universe.Instances.Reverse

import qualified Data.Map as M

repr :: (Bool -> Bool -> Bool) -> String
repr f = fromMaybe noName (M.lookup f names) where
    noName = "No short name for " ++ show f
    names = M.fromList
        [ ((&&), "and")
        , ((||), "or")
        , ((==), "equivalent to")
        ]

这样做的好处是,您不必查看输出列表,也不必像 Joseph 的回答中必须做的那样对描述的函数进行逆向工程;相反,实际的 Haskell 函数(例如 (&&))和我们想要给它的名称(例如 "and")之间存在直接的视觉联系。这是在 ghci 中使用它的一个简单示例:

> repr (&&)
"and"
> repr (/=)
"No short name for [(False,[(False,False),(True,True)]),(True,[(False,True),(True,False)])]"
> repr (\x -> if x then id else not)
"equivalent to"