【问题标题】:How do I automate chains of map lookups in F#?如何在 F# 中自动执行地图查找链?
【发布时间】:2026-02-07 13:15:01
【问题描述】:

我经常有理由在一个映射中查找一些键,然后在另一个映射中查找结果,依此类推,在任意链中,我正在寻求概括该过程。我还希望能够为此目的预先计算地图列表,以表示 AI 项目中可重用的关联链。

显而易见的解决方案似乎是对我称之为“maybe_map”的列表的 map 函数的简单扩展。

let rec maybe_map operation initial_value list =
  match list with
  | []          -> Some(initial_value)
  | first::rest ->
    match operation initial_value first with
    | None                   -> None
    | Some(subsequent_value) -> maybe_map operation subsequent_value rest 

以下是它的使用示例。

let employee_department = Map.ofList([("John", "Sales" ); ("Bob",    "IT"    )])
let department_country  = Map.ofList([("IT",   "USA"   ); ("Sales",  "France")])
let country_currency    = Map.ofList([("USA",  "Dollar"); ("France", "Euro"  )])

let result = maybe_map Map.tryFind "John" [employee_department; department_country; country_currency]

这可以正常工作并产生结果 Some(“Euro”)。当我需要使用地图的域和范围类型不同的数据时,问题就来了。例如,如果我在上面的示例中添加一个汇率映射,即使涉及的单个操作都不会失败,类型检查器也会抱怨。

let exchange_rate = Map.ofList([("Euro", 1.08); ("Dollar", 1.2)])

let result1 = maybe_map Map.tryFind "John" [employee_department; department_country; country_currency; exchange_rate]

类型检查器抱怨 exchange_rate 应该是 Map 类型,尽管 Map.tryFind 的以下应用程序都是有效的。

let result2 = Map.tryFind "John" employee_department
let result3 = Map.tryFind "Euro" exchange_rate

如果我在 C# 中执行此操作,在 Dictionary 类型的数据上定义 Maybe_map 似乎相对简单,显然任何“Duck Typed”语言都没有问题,但我看不出如何在 F# 中执行此操作。

解决这种似乎经常出现的问题的参考是http://codebetter.com/matthewpodwysocki/2009/01/28/much-ado-about-monads-maybe-edition/,它遵循基于单子的方法。其实这里例子中用到的数据都是取自这篇文章。然而,尽管使用 monad 做如此简单的事情看起来就像是用大锤敲碎了坚果,但当地图的域和范围类型不同时,文章中给出的解决方案也会失效。

有谁知道我怎样才能在这方面取得进展,而不会给概念上非常简单的东西带来额外的复杂性?

结语

在考虑了由于这个问题而收到的非常有用的意见后,我们决定这样做。

class chainable
{
  protected Dictionary<IComparable, IComparable> items = new Dictionary<IComparable,IComparable>();

  public chainable() {}

  public chainable(params pair[] pairs) { foreach (pair p in pairs) items.Add(p.key, p.value); }

  public void add(IComparable key, IComparable value) { items.Add(key, value); }

  public IComparable lookup(IComparable key) { if (items.ContainsKey(key)) return items[key]; else return null; }

  public static List<chainable> chain(params chainable[] chainables) { return new List<chainable>(chainables); }

  public static IComparable lookup(IComparable key, List<chainable> chain)
  {
    IComparable value = key;
    foreach (chainable link in chain) { value = link.lookup(value); if (value == null) return null; }
    return value;
  }
}

这个类的第一行实际上是对所需内容的规范,其余的操作直接从它开始。

chainable employee_department = new chainable(new pair("John", "Sales" ), new pair("Bob",    "IT"    ));
chainable department_country  = new chainable(new pair("IT",   "USA"   ), new pair("Sales",  "France"));
chainable country_currency    = new chainable(new pair("USA",  "Dollar"), new pair("France", "Euro"  ));
chainable exchange_rate       = new chainable(new pair("Euro", 1.08    ), new pair("Dollar", 1.2     ));
List<chainable> chain = chainable.chain(employee_department, department_country, country_currency, exchange_rate);
IComparable result    = chainable.lookup("John", chain);

诚然,可链接类可以在 F# 中构建,但这只是使用 F# 语法编写 C#,因此我们决定去掉中间人。似乎 F# 并不总是能够从所需操作的直接描述中推断出您真正需要操作的数据类型。再次感谢您的帮助(抱歉,不能使用 Haskell)。

【问题讨论】:

  • 您的设计是否必须如此注重字符串和字典?部门不能是引用国家对象的对象吗?
  • @svick 不幸的是,映射中涉及的任意“事物”在系统运行之前是未知的。
  • 我没想到你会搬到 Haskell :)

标签: types map f# monads


【解决方案1】:

使用这个辅助函数

let search map = Option.bind (fun k -> Map.tryFind k map)

您可以使用函数组合将正在搜索的地图链接在一起:

let searchAll = 
  search employee_department
  >> search department_country
  >> search country_currency
  >> search exchange_rate

searchAll (Some "John") //Some 1.08

这确保了映射N的值的类型匹配映射N+1的键的类型。我看不到一种搜索list&lt;Map&lt;_,_&gt;&gt; 的方法,而不求助于某些接口(如IDictionary)并插入对box/unbox 的调用以避开类型系统。

顺便说一句,您的maybe_map 函数可以编写

let maybeMap = List.fold (fun v m -> Option.bind (fun k -> Map.tryFind k m) v)

(仍然不适用于不同类型的Maps)

编辑

由于您的地图不是在编译时定义的,并且因类型参数而异,您可以改为在运行时构建查找函数的集合。

let maybeMap = Seq.fold (fun v f -> Option.bind f v)
let tryFind m (v: obj) = Map.tryFind (unbox v) m |> Option.map box

let lookups = ResizeArray() //using ResizeArray to emphasize that this is built dynamically
lookups.Add(tryFind employee_department)
lookups.Add(tryFind department_country)
lookups.Add(tryFind country_currency)
lookups.Add(tryFind exchange_rate)

maybeMap (Some (box "John")) lookups

它不是那么整洁,但符合您的要求。

【讨论】:

  • 谢谢。是的,但正如你所说,不幸的是仍然不支持地图列表,这是关键。我从使用 List.fold 的版本中“展开”了我的 maybe_map 函数,以尝试找到引入类型约束以解决问题并使其更易于讨论的范围。我开始认为这个问题的根源在于首先找到一种方法来构建包含不同类型映射的列表。
  • 您可以构造函数列表而不是映射。我用一个例子更新了我的答案。
  • 是的。当然,当有疑问时添加额外的功能层! (因为没有想到这一点而自责)我将其标记为已回答,因为我可以看到如何使您的解决方案适应我的需求。然而,类型检查有时让一个人在做概念上简单的事情时如此努力,这仍然有点令人失望。我的印象是它无法推断出常见的子类型,这就是我试图让它做的事情。
  • 在一个假设的场景中,如果你有 map #1 &lt;string,float&gt;、#2 &lt;float,int&gt; 和 #3 &lt;int,string&gt;,你可以使用函数组合将连续调用链接到 Map.tryFind,但是我看不到如何为这些映射的列表确定通用基本类型(obj 除外,类似于我的示例)。从理论上讲,您可以使用list&lt;Map&lt;IComparable, IComparable&gt;&gt;,但我不确定它是否能为您的场景带来任何好处。
【解决方案2】:

@Daniel 给出了一个绝妙的解决方案。我想评论一下泛型可以做什么和不能做什么:原始程序是显然类型安全的,那么为什么编译器没有意识到这一点?

以下是关键部分(释义):

let country_currency = Map.ofList([("USA",  "Dollar"); ("France", "Euro")])
let exchange_rate    = Map.ofList([("Euro", 1.08); ("Dollar", 1.2)])
let maps = [country_currency; exchange_rate]
// Error: "... Expecting a Map<string,string> but given a Map<string,float>"    

嗯,F# 列表是同质的:它们只能包含相同类型的值(对象和强制转换不受限制)。但是我们想要不同的类型Map&lt;..., string&gt;Map&lt;string, float&gt;,所以我们不能将映射链放在列表中。不过,我们并不真正关心需要一个列表,我们只需要一些映射集合,其中一个的值类型是下一个的键类型。让我们尝试制作这样的类型。

type Chain<'T,'U> =
    | Link of Chain<'T,'R> * Chain<'R,'U>
    | Node of Map<'T,'U>

但编译器不会让我们这样做:

错误 FS0039:类型参数“R”未定义

理想情况下,我们希望Link 构造函数在'R 中是通用的,即写一些东西 像这样:

    | Link<'R> of Chain<'T,'R> * Chain<'R,'U>

但这也是不允许的:所有用于定义type Chain&lt;...&gt; 的类型参数都必须在&lt;...&gt; 中声明。不受对象和强制转换的影响,这意味着 F# 类型的值只能由固定数量的其他类型组成。但我们不知道需要链接多少种不同类型的 Map!


假设 F#确实 允许 Link 独立于 Chain 是通用的。然后patten匹配允许类型参数'R转义:

match c : Chain<string, float> with 
| Link (c1, c2) -> ... // c1 : Map<string, 'R>,  c2 : Map<'R, float> ? 

当然,c 是由某种实际 类型而不是类型参数'R 构造的,但我们不知道是哪个。在这种情况下,我们说c1c2 具有“存在类型”:我们知道“存在”一些我们可以为上面的'R 输入的特定类型,但我们不知道是哪个。如果我们有这样的存在类型,我们可以写出请求的maybe_map

// maybe_map<'T,'U> : 'T -> Chain<'T,'U> -> Option<'U> 
let rec maybe_map k = function
   | Node m -> Map.tryFind k m
   | Link (c1, c2) ->            // some 'R. c1: Map<'T,'R>, c2: Map<'R,'U>
        let o = maybe_map k c1   // o : ?'R option 
        match o with                
        | None -> None 
        | Some v -> maybe_map v c2   // : Option<'U>

我们无法在 F# 中执行此操作,但 the Haskell people do。让我们看看 Haskell 的全部内容:

{-# LANGUAGE ExistentialQuantification #-}

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

department_country = Map.fromList [("IT",   "USA"   ), ("Sales",  "France")]
country_currency   = Map.fromList [("USA",  "Dollar"), ("France", "Euro")]
exchange_rate      = Map.fromList [("Euro", 1.08),     ("Dollar", 1.2)]

data Chain t u =
    forall r. (Ord r) =>   -- Any type r can be inside the chain
       Link (Chain t r) (Chain r u)
  | Node (Map t u)

lookup :: (Ord t, Ord u) => t -> Chain t u -> Maybe u
lookup k (Node m) = Map.lookup k m
lookup k (Link c1 c2) = case lookup k c1 of
                           Just v -> lookup v c2
                           Nothing -> Nothing

chain = Link (Node department_country) 
             (Link (Node country_currency) (Node exchange_rate))

并尝试一下:

*Main> maybe_map "IT" chain
Just 1.2

【讨论】: