Haskell编程:动态定义函数

7

我正在寻找一种在Haskell中动态定义函数的方法,或者Haskell的惯用等价物,而我显然不知道这个等价物。

场景如下:我有一个tagWithAttrs函数,根据提供的String参数生成新函数。 定义大致如下:

tagWithAttrs :: String -> ([(String, String)] -> [String] -> String)
tagWithAttrs tagName = [...]  -- Implementation ommited to save room.

h1 :: [(String, String)] -> [String] -> String
h1 = tagWithAttrs "h1"

main :: IO ()
main = putStrLn $ h1 [("id", "abc"), ("class", "def")] ["A H1 Test"]

-- Should display '<h1 id="abc" class="def">A H1 Test</h1>'.

到目前为止一切都很好。但是我分配

的那行代码只是众多代码中的一行,因为我必须为我定义的每个HTML标记都这样做。在Python中,我会循环遍历HTML标记名称列表,将的每个相应结果插入返回的字典中。简而言之,我将动态地向符号表中插入新条目。
那么,在Haskell中有什么类似的用法?
顺便说一下,我完全知道我正在复制许多已经存在的库所做的HTML标记工作。我只是为一个玩具项目而这样做:)
编辑:一些发布的解决方案建议仍然依赖逐个定义最终结果标签函数的机制。这违反了DRY原则,否则我就会按照我以前的方式做。我正在尝试规避这种DRY违规。


3
不需要动态函数定义,只需使用一等函数。 - Don Stewart
澄清一下,使用直接函数的唯一方法是为每个标签定义一个函数,或者在数据结构中查找所需的函数。前者代码定义冗长,后者则代码使用冗长。我正在寻找一种我不知道的方式,似乎目前TH是这样的。 - Louis Jackman
1
不要试图将其他语言的解决方案硬塞到Haskell中来解决这个问题;正如Don和Ertes所提到的那样,这可以干净利落地完成,学习如何做到这一点可能会比Template Haskell更能教你关于Haskell和函数式编程的知识。 - Matt Fenwick
1
在这种情况下,我建议重写问题,更清楚地说明您需要什么以及为什么需要它;而不是用另一种语言描述如何解决问题,应该描述原始问题及其背景。 - Matt Fenwick
3
虽然我自己可能会使用代码生成的方法,但是看到不断提出的建议并不能真正解决提问者的问题还是有点令人沮丧。明显的非TH方法是创建一个数据类型data HtmlTag = H1 | H2 | H3 deriving (Eq, Show),然后构建一个地图(如ertes所建议的),键为HtmlTag,使用Show实例和现有的tagWithAttrs函数。然后创建一个新函数tag :: HtmlTag -> [(String,String)] -> [String] -> String,在地图中查找标记,您将使用它作为putStrLn $ tag H1 ...。这仍然有点冗长。 - John L
显示剩余4条评论
5个回答

8
Haskell是静态类型的,这意味着所有符号必须在编译时进行类型检查。所以这意味着您不能在运行时向符号表中添加条目。
您想要的是元编程。其中代码在编译时运行以生成其他代码(您自然而然地感到懒得输入)。这意味着需要类似宏系统的东西。
Haskell没有宏,但有模板Haskell:http://www.haskell.org/haskellwiki/Template_Haskell 与宏一样,您编写一个生成AST的函数。元函数使用您想要使用的函数的名称(在您的情况下是div、ul、li等)并生成具有该名称的函数的AST。
可能有点过度,但如果您真的想这样做,这是一个相对简单的教程:http://playingwithpointers.com/archives/615

谢谢您的建议。我之前考虑过TH,在Haskell中使用它是因为我对该问题的思维方式不正确。最终我可能会使用TH,但我想知道如何在没有TH的情况下实现类似的功能。这样的设计如何在不过多重复的情况下完成,并且不违反类型系统或使用编译时变异? - Louis Jackman
重要的是,如果不使用类似TH的东西,就无法完成它。通过反证法可以证明:如果我们假设可以这样做,那么存在一些代码将函数名“f”与lambda关联起来。现在,“f”必须始终通过类型检查;即生成它的代码必须始终成功,这是在编译时无法保证的。 - Faiz
经过考虑,我想TH似乎是我想要的解决方案。谢谢! - Louis Jackman

6

可以轻松地通过使用一些模板Haskell来完成:

{-# LANGUAGE TemplateHaskell #-}

import Control.Monad (forM)
import Language.Haskell.TH

tagWithAttrs :: String -> ([(String, String)] -> [String] -> String)
tagWithAttrs tagName = undefined

$(forM ["h1", "h2", "h3"] $ \tag ->
   valD (varP (mkName tag)) (normalB [| tagWithAttrs $(stringE tag) |]) [])

main :: IO ()
main = putStrLn $ h1 [("id", "abc"), ("class", "def")] ["A H1 Test"]

这将生成声明h1 = tagWithAttrs "h1"h2 = tagWithAttrs "h2"h3 = tagWithAttrs "h3"等。要添加更多,请将它们添加到列表中。

由于在TH中无法拼接模式,因此代码有点丑陋。否则,我们可以编写类似于[d| $(mkName tag) = tagWithAttrs $(stringE tag) |]的内容。相反,我们必须使用TH组合器手动构建声明。


+1 感谢您提供的使用示例,考虑到我决定采用 TH 这条路线,它会派上用场。 - Louis Jackman

6

如你所知,Haskell是柯里化的,函数是一等公民,因此你不需要任何魔法来实现这个。只要认识到你可以做如下操作:

import qualified Data.Map as M
import Data.Map (Map)
import Data.Text (Text)

type TagName = Text
type TagWithAttrs = Map TagName ([(String, String)] -> [String] -> String)

tagFuncs :: TagWithAttrs
tagFuncs =
    M.fromList $
    ("h1", \xs ys -> zs) :
    ("h2", \xs ys -> zs) :
    {- ... -}
    []

tagWithAttrs :: TagName -> [(String, String)] -> [String] -> String
tagWithAttrs = flip M.lookup tagFuncs

这是一段常规高效的 Haskell 代码。注意:你可能会想把 tagFuncs 定义为 tagWithAttrs 的本地值,使用 where 子句。虽然这样可以让你的代码更美观,但每次调用 tagWithAttrs 都会重新生成映射表。
为了动态地将内容插入到映射表中,你可以将映射表作为参数传递给 tagWithAttrs,而不是作为顶层映射表。另一个选择是使用并发变量,如 MVar 或(更好的选择)TVar

所以有一个地图,当生成标签函数时,通过指定的字符串进行查找,返回每个标签对应的函数。对于 h1,它被定义为地图中的一个条目。对于 h2,你将其定义为地图中的一个条目。对于 h3……它与原始代码完全相同,一遍又一遍地重复自己。我不明白中间地图如何比 OP 更好,但也许是因为我错过了什么。你仍然必须为每个单独的标签定义一个 tagFuncs 条目,对吧?所以这张地图并没有解决任何问题。 - Louis Jackman

4

我认为我会定义一个标签的数据类型:

data Tag = H1 | H2 | H3 ...
    deriving (Show, Eq, Ord, Enum, Bounded)

这是所有标签的单一定义点。
然后定义一个将 Tag 值映射到相应函数的函数:
tag :: Tag -> [(String, String)] -> [String] -> String
tag = tagWithAttrs . show

然后当你想调用 h1, h2, h3 时,你可以调用 tag H1, tag H2, tag H3 等等。

请注意,这与定义函数 tag_h1,tag_h2,tag_h3 没有任何区别; 实际上,你只是有稍微更长的名称(包括空格)。对我来说,这是DRY和“说出你想要的”的理想组合。对我而言,h1 并不像一个函数; 我实际上更希望认为我正在处理一系列数据项上的一个函数,而不是一个庞大的函数集合。

如果我对此的速度不满意(因为编译器可能不会优化掉所有tagWithAttrs 调用)并且我已经确定这是加速我的应用程序的“最低限度”,那么我将查看memoizing tagWithAttrstag,但在内部保持相同的接口。一种快速的可能性是: 预先填充一个包含所有标记的映射表; 你可以使用 EnumBounded 实例来完成这个过程,而不必显式重新列出所有标记(这是使用函数或字符串表示的标记无法做到的)。非严格求值的一个好处是,这可能会导致 tagWithAttrs 被评估恰好一次,用于每个实际使用的标记。

这仍然会在每个 tag 调用上留下一个数据结构查找(除非编译器足够聪明以将其优化掉,这并非不可能)。我怀疑这不会是最重要的性能瓶颈,除非你对程序的其余部分进行了大量的优化。要在编译时执行所有查找(不依赖于优化器),我认为你需要 Template Haskell。在这种情况下,我可能不会走得那么远,因为我真的怀疑我需要它运行得更快(而且我有比我更多的可用计算时间)。但即使我使用模板Haskell在编译时获取查找,我也更喜欢让它看起来像每个标记的单独顶层函数; 我只是觉得“标记和知道如何呈现标记的函数”比“可以调用自身以呈现自身的标记”更自然和灵活。


我喜欢这个解决方案。在使用类型而不是代码生成方面,对我来说,这似乎更符合 Haskell 的惯用方式。而且额外的冗长也并不多,因为列出每个标签的类型构造器所需的键入比将函数分配给以每个标签命名的值要少得多。谢谢。 - Louis Jackman

0
编写一个简单的代码生成器,输入您想要的标签列表,将输出包含为模块。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接