packages feed

simple-atom (empty) → 0.1.0.1

raw patch · 5 files changed

+468/−0 lines, 5 filesdep +basedep +containerssetup-changed

Dependencies added: base, containers

Files

+ Data/Atom/Simple.hs view
@@ -0,0 +1,107 @@+module Data.Atom.Simple+  ( Symbol, intern )+where++import Data.Char ( ord )+import qualified Data.Map as M+import System.IO.Unsafe ( unsafePerformIO )+import Data.IORef ( newIORef, atomicModifyIORef )++-- -------------------------------------------------------------------+-- Public Interface++-- | A 'Symbol'.  This is essentially a 'String', but with different+-- performance characteristics:+--+--  * @O(n)@ creation time (using 'insert')+--+--  * @O(1)@ equality comparison.+--+--  * @O(1)@ comparison (in practice).  The result of 'compare' is+--    independent of evaluation order.+--+-- It is currently implemented as follows.+--+--  * Each symbol contains a unique integer, which allows @O(1)@+--    comparison.+--+--  * Each symbol contains an infinite chain of hashes, these are used+--    for comparison.  In practice, it is very rare that more than the+--    first of those hashes is ever evaluated.  The first hash is+--    cached, so that most comparisons will not need any indirections.+--+--  * The 'String' representation of the symbol.  Use 'show' to return+--    it.  At any time, there will be only one symbol of a given name+--    in memory.+--+data Symbol = MkSymbol {-# UNPACK #-} !Int   -- identity+                       {-# UNPACK #-} !Int   -- 1st hash+                                      [Int]  -- other hashes+                                      String -- name++instance Show Symbol where+  show (MkSymbol i h _ s) = "<" ++ show i ++ "," ++ show h ++ ">" ++ s++instance Eq Symbol where+  MkSymbol i1 _ _ _ == MkSymbol i2 _ _ _ = i1 == i2++instance Ord Symbol where+  MkSymbol i1 c1 cs1 _ `compare` MkSymbol i2 c2 cs2 _+    | i1 == i2   = EQ+    | otherwise  =+       case c1 `compare` c2 of+         EQ -> cs1 `compare` cs2+         ans -> ans++-- | Turn a 'String' into a 'Symbol'.+--+-- Note, however, that this function contains a space leak.  It has+-- internal state (the symbol table) but is referentially transparent.+-- Unfortunately, there is no way to delete items from the symbol+-- table.+--+-- (This function is, of course, thread-safe.)+intern :: String -> Symbol+intern =+  unsafePerformIO $+    do tab <- newIORef (SymTbl 0 M.empty)+       return $ \s ->+         unsafePerformIO $+           atomicModifyIORef tab $ \tbl@(SymTbl n t) ->+             case M.lookup s t of+               Just sym -> (tbl, sym)+                +               Nothing ->+                 let n' = n + 1+                     sym = mkSymbol n s+                 in (SymTbl n' (M.insert s sym t), sym)+{-# NOINLINE intern #-}++-- -------------------------------------------------------------------+-- TODO: Use a trie?  Or a hash table?+data SymTbl = SymTbl !Int !(M.Map String Symbol)++mkSymbol :: Int -> String -> Symbol+mkSymbol sym_identity str = MkSymbol sym_identity h hashes str+  where+    ints = map ord str+    (h:hashes) = [ hash p ints | p <- bigprimes ]++---------------------------------------------------------------------------+-- hash stuff++hash :: Int -> [Int] -> Int+hash p []     = 0+hash p (i:is) = i + p * hash p is++-- Note: While these aren't very efficient it is unlikely that more+-- than the first couple of elements are ever evaluated during the+-- whole run of a program.+primes, bigprimes :: [Int]+primes = 2 : [ n | n <- [3..], all (n !/) (takeWhile (<= sqr n) primes) ]+ where+  a !/ b = a `mod` b /= 0+  sqr    = floor . sqrt . fromIntegral++bigprimes = dropWhile (<= 258) primes+
+ Data/Atom/UF.hs view
@@ -0,0 +1,307 @@+{-# LANGUAGE BangPatterns #-}+{-# OPTIONS_GHC -funbox-strict-fields #-}+-- | +-- Module      : Data.Atom.UF+-- Copyright   : (c) Thomas Schilling 2010+-- License     : BSD-style+--+-- Maintainer  : nominolo@gmail.com+-- Stability   : experimental+-- Portability : portable+--+-- Symbols without a central symbol table.+--+-- Symbols provide the following efficient operations:+--+--  - /O(1)/ equality comparison (in practise)+--  - /O(1)/ ordering comparison (in practise)+--  - /O(n)/ creation+--+-- This can be implemented by using a global variable mapping strings+-- to symbols and a counter assigning ids to symbols.  However, this+-- has two problems:+--+--  1. It has a space leak.  No symbols can ever be removed from this+--     table.  For example, if we add the symbol @\"foo\"@ the first+--     time it might get assigned id 1, if we then delete it and+--     insert it again it might get assigned id 42.  However, there+--     may still be symbols in memory which got assigned id 1.+--     Instead, symbols should be garbage collected like other data.+--     Using weak pointers has bad effects on performance due to+--     garbage collector overhead.+--+--  2. It is not reliable to compare symbols created using different+--     symbol tables.  They would most likely get assigned different+--     ids.+--+-- This implementation of symbols allows *optional* use of a symbol+-- table.  If a symbol table is used, this implementation will tend to+-- use less memory and its operations will be a little bit faster at+-- the beginning.  For longer runs, it won't make a big difference+-- though, since the representation is self-optimising.+--+-- Inspired by Richard O'Keefe's message to Erlang's eeps mailing list+-- <http://www.erlang.org/cgi-bin/ezmlm-cgi/5/057>, which in turn was+-- inspired by the Logix implementation of Flat Concurrent Prolog.+--+--+-- * Implementation+--+-- Each symbol is represented a pointer to the symbol info, which+-- consists of:+--+--   * a 'String'+--   * a 'Hash'+--   * a null-able parent pointer to an equivalent symbol info+--+-- Creating the same symbol twice will at first be represented as two+-- different entities.+--+-- @+--            .----+-------+-----.+--   A -----> | 42 | "foo" | nil |+--            '----+-------+-----'+--   B --.+--       '--> .----+-------+-----.+--   C -----> | 42 | "foo" | nil |+--            '----+-------+-----'+-- @+--+-- (Note that @A@, @B@ and @C@ are @IORefs@.)+--+-- When comparing @A@ and @B@ we use the following properties:+--+--  1. If @A@ and @B@ are identical then they must be equal.+--  +--  2. If they point to the same object, they must equal.+--  +--  3. If they have different hashes, they are different.+--+-- Unless there is a hash collision, we can decide equality and+-- ordering for all symbols that have been built with the same hash+-- table.+--+-- If the two objects have no parent, have the same hash, and the same+-- string, we now make one the first the parent of the other and+-- update the pointer of @B@ accordingly.  If there are no references+-- to the second object left it can now be garbage collected.+--+-- If an object already has a parent pointer we follow each object's+-- parents to the roots and compare the roots.  This process might+-- again result in updates to @A@ or @B@ and various parent pointers.+--+-- In the example above, after @A == B@ we have:+--+-- @+--            .----+-------+-----.+--   A -----> | 42 | "foo" | nil |+--       .--> '----+-------+-----'+--   B --'                    ^+--            .----+-------+--|--.+--   C -----> | 42 | "foo" |  *  |+--            '----+-------+-----'+-- @+--+-- After @C == A@ or @C == B@ we have.+--+-- @+--   A -----> .----+-------+-----.+--       .--> | 42 | "foo" | nil |+--   B --'.-> '----+-------+-----'+--        |                   ^+--        |   .----+-------+--|--.+--   C ---'   | 42 | "foo" |  *  |+--            '----+-------+-----'+-- @+--+-- The second object will now be garbage collected.+--+-- In fact, after the first @A == B@, the remaining updates could use+-- some help from the garbage collector.  This could be done by+-- somehow forcibly (and unsafely) replacing the second object by an+-- update frame and then rely on the GC's indirection shortening+-- feature.  This is /very/ unsafe, since some code may rely \"know\"+-- that the object is already evaluated.  E.g., C's pointer could be+-- tagged (c.f. \"Faster Laziness Using Dynamic Pointer Tagging\").+-- It /might/ work if we can match the physical layout of both+-- structures, but it's equally likely that hell freezes over, so I'll+-- leave that as an exercise for more braver hackers.+--+-- * TODO+--+--  - generalise to arbitrary hashable objects.  need not be+--    restricted to 'String'.+--+--  - make thread-safe.  (we only need a lock for the uncommon cases)+--+--  - make sure the pointer update code is correct and has no bad+--    cases+--+--  - implement IntMap variant\/wrapper that respects that two+--    different objects may have the same key (however unlikely).+-- +module Data.Atom.UF +  ( Symbol, intern, internInto, SymTab(..) )+where++import Data.Word ( Word32 )+import Data.Char ( ord )+import Data.Bits ( xor )+import Data.IORef+import System.IO.Unsafe+import Control.Monad -- ( unless )+import System.Mem.Weak+import System.Mem+import Data.Maybe++-- -------------------------------------------------------------------+-- Public API:++-- | A symbol.+newtype Symbol = Symbol (IORef SymbolInfo)+instance Eq Symbol where x == y = cmpSymbol x y == EQ+instance Ord Symbol where compare = cmpSymbol+instance Show Symbol where show = showSym++-- | Create a new local symbol.  For best performance use+-- 'internInto' together with a symbol table / map.+intern :: String -> Symbol++class SymTab s where+  lookupSymbol :: s -> String -> Maybe Symbol+  insertSymbol :: String -> Symbol -> s -> s++-- | Insert a symbol into an existing table.+internInto :: SymTab s => s -> String -> (s, Symbol)++-- -------------------------------------------------------------------+-- Internals++data SymbolInfo =+  SymInfo {-# UNPACK #-} !Word32  -- hash+          {-# UNPACK #-} !(IORef Link) -- parent [really unpack]?+          String++type Link = Maybe SymbolInfo+++internInto st str =+  case lookupSymbol st str of+    Just sym -> (st, sym)+    _        -> let sym = intern str in+                (insertSymbol str sym st, sym)++showSym :: Symbol -> String+showSym (Symbol r) = unsafePerformIO $ do+  -- dupable/inline is fine, too, since the string never changes+  (SymInfo _ _ str) <- readIORef r+  return str++intern s = unsafePerformIO $ do+  lnk <- newIORef Nothing+  r <- newIORef $ SymInfo (hash s) lnk s+  return (Symbol r)++mkSymbolInfo :: String -> SymbolInfo+mkSymbolInfo s = unsafePerformIO $ do+  lnk <- newIORef Nothing+  return $ SymInfo (hash s) lnk s++cmpSymbol :: Symbol -> Symbol -> Ordering+cmpSymbol (Symbol r1) (Symbol r2)+  | r1 == r2 = EQ+  | otherwise = unsafePerformIO $ do+      -- We only read.  It should be safe to use unsafeInlineIO for+      -- the two reads.+      sym1@(SymInfo h1 l1 s1) <- readIORef r1+      sym2@(SymInfo h2 l2 s2) <- readIORef r2+      case h1 `compare` h2 of+        -- If the hashes are different they cannot be the same symbol+        LT -> return LT+        GT -> return GT+        EQ+         | sameSym sym1 sym2 ->+          -- The two references are not the same, but they point to+          -- the same object.  That's fine, we can't optimise any+          -- further.+           return EQ++        -- END OF COMMON CASE+        -- +        -- If the symbols have been built using the same symbol table+        -- we will only reach this case if we have a hash collision or+        -- the symbols were built from different symbol tables.+        --+        -- TODO: Extract into NOINLINE function, wrap unsafePerformIO,+        -- and use an MVar-based lock.++         | otherwise -> do+          -- The hashes are the same.  It could be a collision, or the+          -- symbol was created using a different symbol table.+          --+          -- Case 1: The symbols have already be joined, but this+          -- Symbol's IORef still points to the old version.  We can+          -- determine this by following the union/find structure.+          rep1 <- repr sym1+          rep2 <- repr sym2+          let string_cmp = s1 `compare` s2  -- lazy!+          if sameSym rep1 rep2 || string_cmp == EQ then do+             -- They should in fact be the same symbol.  Update the+             -- atoms and the symbol infos if necessary.+             -- TODO: Use MVar / lock.+             unless (sameSym sym1 rep1) $ do+               writeIORef r1 rep1+               writeIORef l1 (Just rep1)  -- path shortening+             unless (sameSym sym2 rep1) $ do+               writeIORef r2 rep1+               writeIORef l2 (Just rep1)+             return EQ+            else do+              -- They are not the same, and they shouldn't+              return string_cmp+{-# NOINLINE cmpSymbol #-}++-- We abuse the fact that IORefs give us an identity (i.e., observable+-- sharing) and that we need the IORef anyway.+sameSym :: SymbolInfo -> SymbolInfo -> Bool+sameSym (SymInfo _ r1 _) (SymInfo _ r2 _) = r1 == r2++repr :: SymbolInfo -> IO SymbolInfo+repr sym@(SymInfo _ r _) = do+  parent <- readIORef r   -- TODO: perform path shortening.+  case parent of+    Nothing -> return sym+    Just sym' -> repr sym'++test1 = do+  let s1@(Symbol r1) = intern "foo"+      s2@(Symbol r2) = intern "foo"+  print $ r1 == r2   -- should be False+                     +  -- create a weak reference to the second symbol, so we can observe+  -- when it is garbage collected+  w <- mk_weak =<< readIORef r2++  print $ s1 == s2   -- should print True+  print =<< liftM2 sameSym (readIORef r1) (readIORef r2) -- should print True+  putStrLn "GCing"+  performGC          -- this should print goodbye, representing the+                     -- fact that the second symbol has been garbage+                     -- collected.+  print . isJust =<< deRefWeak w  -- should print False (object has been collected)+ where+   mk_weak o = mkWeakPtr o (Just (putStrLn "goodbye"))++-- -------------------------------------------------------------------++-- Fowler / Noll / Vo (FNV) hash.  Original code expected 'unsigned+-- char' input.  Don't know whether it behaves worse for unicode+-- chars.+hash :: String -> Word32+hash str = go magic_start (map ord str)+  where+    magic_start = 2166136261 :: Word32+    go :: Word32 -> [Int] -> Word32+    go !h [] = h+    go !h (c:cs) =+        go ((h * 16777619) `xor` fromIntegral c) cs
+ LICENSE view
@@ -0,0 +1,20 @@+Paradox/Equinox -- Copyright (c) 2003-2007, Koen Claessen, Niklas Sorensson++Permission is hereby granted, free of charge, to any person obtaining a+copy of this software and associated documentation files (the+"Software"), to deal in the Software without restriction, including+without limitation the rights to use, copy, modify, merge, publish,+distribute, sublicense, and/or sell copies of the Software, and to+permit persons to whom the Software is furnished to do so, subject to+the following conditions:++The above copyright notice and this permission notice shall be included+in all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ simple-atom.cabal view
@@ -0,0 +1,32 @@+Name:            simple-atom+Version:         0.1.0.1+License:         BSD3+License-File:    LICENSE+Author:          Koen Claessen, Niklas Sorensson+Maintainer:      Thomas Schilling <nominolo@googlemail.com>+Homepage:        http://github.com/nominolo/simple-atom+Synopsis:        Atom (or symbol) datatype for fast comparision and sorting.+Description:+  This module provides an abstract datatype for atoms, such that:+  .+   * Each atom string is only in memory once+   * @O(n)@ creation time+   * @O(1)@ equality-comparison+   * @O(1)@ (in practice) ord-comparison+   * @Ord@-comparison results are independent on evaluation order+  .+  This module is thread-safe.+                 +Category:        Data, Compilers/Interpreters, Parsing+Stability:       provisional+Build-Type:      Simple+Cabal-Version:   >= 1.6++Library+  Build-Depends:+    base          >= 3.0 && < 4.7,+    containers    >= 0.2 && < 0.6++  exposed-modules:+    Data.Atom.Simple+    Data.Atom.UF