Functional programming 如何在函数式语言中实现哈希表?

Functional programming 如何在函数式语言中实现哈希表?,functional-programming,hashtable,Functional Programming,Hashtable,有没有办法用纯函数式语言高效地实现哈希表?似乎对哈希表的任何更改都需要创建原始哈希表的副本。我一定错过了什么。哈希表是非常重要的数据结构,如果没有它们,编程语言将受到限制。哈希表可以用Haskell中的ST monad之类的东西来实现,它基本上将IO操作包装在一个纯功能接口中。它通过强制按顺序执行IO操作来实现这一点,因此它保持了引用透明性:您无法访问哈希表的旧“版本” 见: 有没有办法用纯函数式语言高效地实现哈希表 哈希表是抽象“字典”或“关联数组”数据结构的具体实现。所以我想你真的想问一下纯

有没有办法用纯函数式语言高效地实现哈希表?似乎对哈希表的任何更改都需要创建原始哈希表的副本。我一定错过了什么。哈希表是非常重要的数据结构,如果没有它们,编程语言将受到限制。

哈希表可以用Haskell中的ST monad之类的东西来实现,它基本上将IO操作包装在一个纯功能接口中。它通过强制按顺序执行IO操作来实现这一点,因此它保持了引用透明性:您无法访问哈希表的旧“版本”

见:

有没有办法用纯函数式语言高效地实现哈希表

哈希表是抽象“字典”或“关联数组”数据结构的具体实现。所以我想你真的想问一下纯函数字典与命令式哈希表相比的效率

似乎对哈希表的任何更改都需要创建原始哈希表的副本

是的,哈希表本质上是必需的,没有直接的纯函数等价物。也许最类似的纯函数字典类型是散列,但由于分配和间接寻址,它们比散列表慢得多

我一定错过了什么。哈希表是非常重要的数据结构,没有它们,编程语言将受到限制

字典是一种非常重要的数据结构(尽管值得注意的是,在Perl在20世纪90年代使其流行之前,字典在主流中很少见,因此人们几十年来一直在编写代码,而没有使用字典)。我同意哈希表也很重要,因为它们通常是迄今为止最有效的字典

有许多纯功能词典:

  • 平衡树(红黑、AVL、重量平衡、手指树等),例如OCaml和F中的
    Map
    ,以及Haskell中的
    Data.Map

  • 散列,例如Clojure中的
    PersistentHashMap

但是这些纯粹的函数字典都比一个合适的哈希表慢得多(例如.NET
字典

注意Haskell基准测试将哈希表与纯函数字典进行比较,声称纯函数字典在性能上具有竞争力。正确的结论是Haskell的哈希表效率很低,几乎和纯函数字典一样慢。例如,如果你与.NET进行比较,你会发现


我认为,要真正总结关于Haskell性能的结论,您需要测试更多的操作,使用非荒谬的键类型(双重键,什么?),不要无缘无故地使用
-N8
,并与同样限制其参数类型的第三种语言(如Java)进行比较(因为Java在大多数情况下具有可接受的性能),看看这是装箱的常见问题还是GHC运行时的一些更严重的错误。这些都是这样的(并且大约是当前哈希表实现的2倍)


这正是我所指的那种错误信息。在这种情况下,不要注意Haskell的哈希表,只看最快的哈希表的性能(即不是Haskell)而且是最快的纯函数词典。

现有的答案都有很好的分享点,我想我只需要在等式中添加一段数据:比较几种不同关联数据结构的性能

测试包括顺序插入、查找和添加数组元素。此测试并不严格,也不应该如此,它只是一个预期的指示

首先在Java中使用
HashMap
非同步
Map
实现:

import java.util.Map;
import java.util.HashMap;

class HashTest {
    public static void main (String[] args)
    {
        Map <Integer, Integer> map = new HashMap<Integer, Integer> ();
        int n = Integer.parseInt (args [0]);
        for (int i = 0; i < n; i++)
            {
                map.put (i, i);
            }

        int sum = 0;
        for (int i = 0; i < n; i++)
            {
                sum += map.get (i);
            }


        System.out.println ("" + sum);
    }
}
最后,使用来自hackage的不可变的
HashMap
实现(来自
HashMap
包):

modulemain其中
导入数据列表(foldl')
将限定的Data.HashMap导入为HashMap
导入系统。环境
main::IO()
main=do
n hashmap hashmap.!i+s)0[0..n-1]
打印x
检查n=10000000的性能,我发现总运行时间如下:

  • Java HashMap--24.387s
  • Haskell哈希表——7.705s,GC中41%的时间(
  • Haskell HashMap——9.368s,GC中62%的时间
将其降至n=1000000,我们得到:

  • Java HashMap——0.700s
  • Haskell哈希表--0.723s
  • Haskell HashMap--0.789s
这很有趣,原因有二:

  • 性能通常非常接近(除了Java偏离超过1M个条目的情况)
  • 收集花费了大量的时间!(在n=100000000的情况下杀死Java)
  • 这似乎表明,在Haskell和Java这样的语言中,已经装箱映射键的语言会从这个装箱中看到巨大的成功。不需要或者可以取消装箱键和值的语言可能会看到好几倍的性能

    显然,这些实现并不是最快的,但我要说的是,使用Java作为基线,它们至少在许多方面都是可以接受/可用的(尽管可能更熟悉Java wisdom的人会说HashMap是否合理)

    我要指出的是,与哈希表相比,Haskell哈希映射占用了大量空间


    Haskell程序是用GHC 7.0.3和
    -O2-threaded
    编译的,并且运行时只使用运行时GC统计的
    +RTS-s
    标志。Java是用OpenJDK 1.7编译的。

    您缺少的是严重高估了哈希表的重要性。具体的数据结构并不重要,它们的性能特征是ristics是的。@camccann:Nam
    {-# LANGUAGE ScopedTypeVariables, BangPatterns #-}
    module Main where
    
    import Control.Monad
    import qualified Data.HashTable.IO as HashTable
    import System.Environment
    
    main :: IO ()
    main = do
      n <- read `fmap` head `fmap` getArgs
      ht :: HashTable.BasicHashTable Int Int <- HashTable.new
      mapM_ (\v -> HashTable.insert ht v v) [0 .. n - 1]
      x <- foldM (\ !s i -> HashTable.lookup ht i >>=
                   maybe undefined (return . (s +)))
           (0 :: Int) [0 .. n - 1]
      print x
    
    module Main where
    
    import Data.List (foldl')
    import qualified Data.HashMap as HashMap
    import System.Environment
    
    main :: IO ()
    main = do
      n <- read `fmap` head `fmap` getArgs
      let
        hashmap = 
            foldl' (\ht v -> HashMap.insert v v ht) 
               HashMap.empty [0 :: Int .. n - 1]
      let x = foldl' (\ s i -> hashmap HashMap.! i + s) 0 [0 .. n - 1]
      print x