packages feed

edit-distance-linear (empty) → 0.2.0.1

raw patch · 9 files changed

+268/−0 lines, 9 filesdep +QuickCheckdep +arraydep +basesetup-changed

Dependencies added: QuickCheck, array, base, bytestring, criterion, edit-distance, edit-distance-linear, hspec, text, text-metrics

Files

+ ChangeLog.md view
@@ -0,0 +1,13 @@+# Changelog for edit-distance-linear++## v0.2.0.1++* Made the test executable and the LLVM codegen optional.++## v0.2.0.0++* Reimplemented with mutable arrays and rather low-level optimizations.++## v0.1.0.0++* Initial implementation with Data.Vector.
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Georg Rudoy (c) 2019++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Georg Rudoy nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,18 @@+# edit-distance-linear++[![Build Status][travis-badge]][travis]++The pure Haskell implementation of the Levenshtein edit distance, with linear space complexity.++## Comparison++There are already several other existing implementations, but the goals and design decisions vary. In particular, this package is intended to be used to:+* compare long strings (think tens of thousands of characters), driving the implementation to live in the `ST` monad and aim at linear space complexity to lower GC pressure;+* not care about Unicode, thus accepting `ByteString`s and comparing them byte-by-byte rather than character-by-character (or glyph-by-glyph, or whatever is the right notion of an edit for Unicode).++Among the alternatives:+* [text-metrics](http://hackage.haskell.org/package/text-metrics) — uses a similar algorithm, but cares about Unicode, making it 4-5 times slower.+* [edit-distance](http://hackage.haskell.org/package/edit-distance) — uses a very different algorithm (which we might implement here one day with huge potential benefits), which tends to consume more memory (I'm not up for estimating its space asymptotics, though).++[travis]:        <https://travis-ci.org/0xd34df00d/edit-distance-linear>+[travis-badge]:  <https://travis-ci.org/0xd34df00d/edit-distance-linear.svg?branch=master>
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ app/Main.hs view
@@ -0,0 +1,12 @@+module Main where++import qualified Data.ByteString.Char8 as BS+import Text.EditDistance.Linear++main :: IO ()+main = do+  let s1 = BS.replicate 10000 'a'+  let s2 = s1+  let s3 = BS.replicate 10000 'b'+  print $ levenshteinDistance s1 s2+  print $ levenshteinDistance s1 s3
+ bench/Bench.hs view
@@ -0,0 +1,26 @@+{-# LANGUAGE BangPatterns, OverloadedStrings #-}++import qualified Data.ByteString.Char8 as BS+import qualified Data.Text as T+import Criterion.Main+import Data.Text.Metrics++import Text.EditDistance.Linear++main :: IO ()+main = defaultMain+  [ bgroup "same string/ed-linear"      [ bench (show num) $ nf (\s -> levenshteinDistance s s) $ BS.replicate num 'a'+                                        | num <- nums+                                        ]+  , bgroup "same string/text-metrics"   [ bench (show num) $ nf (uncurry levenshtein) (T.replicate num "a", T.replicate num "a")+                                        | num <- nums+                                        ]+  , bgroup "diff strings/ed-linear"     [ bench (show num) $ nf (uncurry levenshteinDistance) (BS.replicate num 'a', BS.replicate num 'b')+                                        | num <- nums+                                        ]+  , bgroup "diff strings/text-metrics"  [ bench (show num) $ nf (uncurry levenshtein) (T.replicate num "a", T.replicate num "b")+                                        | num <- nums+                                        ]+  ]+  where+    nums = [ 1000, 2000, 5000, 10000, 20000 ]
+ edit-distance-linear.cabal view
@@ -0,0 +1,106 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.31.2.+--+-- see: https://github.com/sol/hpack+--+-- hash: 52175124e00b64f40bb9551356b58a8b08d4d0f2b7ec2366fa339ffc53e59081++name:           edit-distance-linear+version:        0.2.0.1+synopsis:       Efficient implementation Levenshtein distance in linear memory.+description:    Please see the README on GitHub at <https://github.com/0xd34df00d/edit-distance-linear#readme>+category:       Algorithms+homepage:       https://github.com/0xd34df00d/edit-distance-linear#readme+bug-reports:    https://github.com/0xd34df00d/edit-distance-linear/issues+author:         Georg Rudoy+maintainer:     0xd34df00d@gmail.com+copyright:      2019 Georg Rudoy+license:        BSD3+license-file:   LICENSE+build-type:     Simple+extra-source-files:+    README.md+    ChangeLog.md++source-repository head+  type: git+  location: https://github.com/0xd34df00d/edit-distance-linear++flag llvm+  description: Use the LLVM code generator (strongly recommended)+  manual: True+  default: True++flag with-executable+  description: Build the test executable (a fast and dirty way of benchmarking, for the package development only)+  manual: True+  default: False++library+  exposed-modules:+      Text.EditDistance.Linear+  other-modules:+      Paths_edit_distance_linear+  hs-source-dirs:+      src+  ghc-options: -Wall+  build-depends:+      array+    , base >=4.7 && <5+    , bytestring+  if flag(llvm)+    ghc-options: -fllvm+  default-language: Haskell2010++executable edit-distance-linear-exe+  main-is: Main.hs+  other-modules:+      Paths_edit_distance_linear+  hs-source-dirs:+      app+  ghc-options: -Wall -threaded -rtsopts+  build-depends:+      array+    , base >=4.7 && <5+    , bytestring+    , edit-distance-linear+  if !(flag(with-executable))+    buildable: False+  default-language: Haskell2010++test-suite edit-distance-linear-test+  type: exitcode-stdio-1.0+  main-is: Spec.hs+  other-modules:+      Paths_edit_distance_linear+  hs-source-dirs:+      test+  ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N+  build-depends:+      QuickCheck+    , array+    , base >=4.7 && <5+    , bytestring+    , edit-distance+    , edit-distance-linear+    , hspec+  default-language: Haskell2010++benchmark edit-distance-linear-bench+  type: exitcode-stdio-1.0+  main-is: Bench.hs+  other-modules:+      Paths_edit_distance_linear+  hs-source-dirs:+      bench+  ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N+  build-depends:+      array+    , base >=4.7 && <5+    , bytestring+    , criterion+    , edit-distance-linear+    , text+    , text-metrics+  default-language: Haskell2010
+ src/Text/EditDistance/Linear.hs view
@@ -0,0 +1,37 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE FlexibleContexts #-}++module Text.EditDistance.Linear where++import qualified Data.Array.Base as A(unsafeRead, unsafeWrite)+import qualified Data.Array.ST as A+import qualified Data.ByteString.Char8 as BS+import qualified Data.ByteString.Unsafe as BU+import Control.Monad.ST++levenshteinDistance :: BS.ByteString -> BS.ByteString -> Int+levenshteinDistance s1 s2 = runST $ do+  v0Init <- A.newListArray (0, n) [0..]+  v1Init <- A.newArray_ (0, n)+  loop 0 v0Init v1Init+  A.unsafeRead (if even m then v0Init else v1Init) n++  where+    m = BS.length s1+    n = BS.length s2++    loop :: Int -> A.STUArray s Int Int -> A.STUArray s Int Int -> ST s ()+    loop !i !v0 !v1 | i == m = pure ()+                    | otherwise = do+      A.unsafeWrite v1 0 (i + 1)+      let !s1char = s1 `BU.unsafeIndex` i+      let go !j !prev | j == n = pure ()+                      | otherwise = do+            delCost <- v0 `A.unsafeRead` (j + 1)+            substCostBase <- v0 `A.unsafeRead` j+            let !substCost = if s1char == s2 `BU.unsafeIndex` j then 0 else 1+            let !res = min (substCost + substCostBase) $ 1 + min delCost prev+            A.unsafeWrite v1 (j + 1) res+            go (j + 1) res+      go 0 (i + 1)+      loop (i + 1) v1 v0
+ test/Spec.hs view
@@ -0,0 +1,24 @@+{-# LANGUAGE ViewPatterns #-}++import qualified Data.ByteString.Char8 as BS+import qualified Text.EditDistance as TED+import Test.Hspec+import Test.QuickCheck++import Text.EditDistance.Linear++baseLevDist :: String -> String -> Int+baseLevDist = TED.levenshteinDistance TED.defaultEditCosts++main :: IO ()+main = hspec $ do+  describe "Obvious cases" $ do+    it "Edit distance to self is always 0" $+      property $ \(BS.pack -> s) -> levenshteinDistance s s `shouldBe` 0+    it "Edit distance to completely different string is strictly positive" $ do+      property $ \(BS.pack . getNonEmpty -> s) -> levenshteinDistance s (BS.map succ s) > 0+    it "Edit distance to completely different string is not greater than string's length" $ do+      property $ \(BS.pack -> s) -> levenshteinDistance s (BS.map succ s) <= BS.length s+  describe "Comparing to edit-distance" $+    it "Edit distances always match" $+      property $ \(getASCIIString -> s1) (getASCIIString -> s2) -> levenshteinDistance (BS.pack s1) (BS.pack s2) `shouldBe` baseLevDist s1 s2