commit f8f05a731bcbeb3cd14e4e928b43ca6767010122 Author: pena Date: Tue May 7 18:57:32 2019 +0200 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c9c859 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +_build/ +_coverage/ +*.merlin +*.install diff --git a/.ocamlformat b/.ocamlformat new file mode 100644 index 0000000..f146225 --- /dev/null +++ b/.ocamlformat @@ -0,0 +1,43 @@ +version=0.26.2 +assignment-operator=end-line +break-cases=fit +break-fun-decl=wrap +break-fun-sig=wrap +break-infix=wrap +break-infix-before-func=false +break-separators=before +break-sequences=true +cases-exp-indent=2 +cases-matching-exp-indent=normal +doc-comments=before +doc-comments-padding=2 +doc-comments-tag-only=default +dock-collection-brackets=false +exp-grouping=preserve +field-space=loose +if-then-else=compact +indicate-multiline-delimiters=space +indicate-nested-or-patterns=unsafe-no +infix-precedence=indent +leading-nested-match-parens=false +let-and=sparse +let-binding-spacing=compact +let-module=compact +margin=80 +max-indent=2 +module-item-spacing=sparse +ocaml-version=4.14.0 +ocp-indent-compat=false +parens-ite=false +parens-tuple=always +parse-docstrings=true +sequence-blank-line=preserve-one +sequence-style=terminator +single-case=compact +space-around-arrays=true +space-around-lists=true +space-around-records=true +space-around-variants=true +type-decl=sparse +wrap-comments=false +wrap-fun-args=true diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000..3d83f72 --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,8 @@ +## 0.2 - 2024-10-22 + +- add inlined hint +- update cache API to be compatible with latest OCaml + +## 0.0.1 - 2019-11-21 + +First release diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..fbb1828 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,8 @@ +The ISC License (ISC) +===================== + +Copyright © 2019-2020, pena + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..25c4bf0 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# memo + +memo is an [OCaml] library for [memoïzation]. + +- [LICENSE] +- [CHANGELOG] + +[CHANGELOG]: ./CHANGELOG.md +[LICENSE]: ./LICENSE.md + +[memoïzation]: https://en.wikipedia.org/wiki/Memoization +[OCaml]: https://en.wikipedia.org/wiki/OCaml diff --git a/doc/dune b/doc/dune new file mode 100644 index 0000000..b446abb --- /dev/null +++ b/doc/dune @@ -0,0 +1,3 @@ +(documentation + (package memo) + (mld_files usage index)) diff --git a/doc/index.mld b/doc/index.mld new file mode 100644 index 0000000..f7c4661 --- /dev/null +++ b/doc/index.mld @@ -0,0 +1,7 @@ +{0 memo} + +[memo] is an {{:https://en.wikipedia.org/wiki/OCaml}OCaml} library for {{:https://en.wikipedia.org/wiki/Memoization}memoïzation}. + +It provides easy ways to memoïze a function, in a type-safe and modular way and the ability to get forgetful memoïzation. + +See {{:usage.html} usage} for a short explanation on how to use the library and {{:Memo/index.html} Memo} for a more detailled explanation. diff --git a/doc/usage.mld b/doc/usage.mld new file mode 100644 index 0000000..b079969 --- /dev/null +++ b/doc/usage.mld @@ -0,0 +1,117 @@ +{0 Usage} + +If you had a function [fibo] defined like this: + +{[ +let rec fibo x = + if x < 0 then invalid_arg "fibo"; + if x < 2 then x + else fibo (x - 1) + fibo (x - 2) +]} + +There's many different ways to memoïze it. + +{1:simple_memo Simple memoïzation} + +The easiest one is to rewrite it like this: + +{[ +let fibo = Memo.memo (fun fibo x -> + if x < 0 then invalid_arg "fibo"; + if x < 2 then x + else fibo (x - 1) + fibo (x - 2)) +]} + +It'll use the {!module:Hashtbl} module from {!module:Stdlib} directly. + +I'd like to thank {{:https://www.lri.fr/~conchon/}Sylvain Conchon} who taught me memoïzation and how to write this [memo] function when I was his student. + +{1:custom_memo Using your own type, [equal] and [hash] functions} + +We provide a {!module:Memo.Make} functor. It can be useful in case you don't want to use polymorphic equality or you are doing things like {{:https://en.wikipedia.org/wiki/Hash_consing}hash consing} and you know how to compare or hash your type more efficiently. + +{[ +let module Mem = Memo.Make(struct + type t = int + let equal = (=) + let hash = Hashtbl.hash +end) + +let fibo = Mem.memo (fun fibo x -> + if x < 0 then invalid_arg "fibo"; + if x < 2 then x + else fibo (x - 1) + fibo (x - 2)) +]} + +{1:forgetful_memo Forgetful memoïzation} + +We provide a {!module:Memo.MakeWeak} functor. It works like the previous one, but the bindings in the memoïzation cache will be weak, allowing the garbage collector to remove them if they are not used somewhere else. + +{[ +let module Mem = Memo.MakeWeak(struct + type t = int + let equal = (=) + let hash = Hashtbl.hash +end) + +let fibo = Mem.memo (fun fibo x -> + if x < 0 then invalid_arg "fibo"; + if x < 2 then x + else fibo (x - 1) + fibo (x - 2)) +]} + +I'd like to thank {{:https://www.lri.fr/~filliatr/}Jean-Christophe Filliâtre} who taugh me forgetful memoïzation when I was doing research on {{:https://en.wikipedia.org/wiki/Binary_decision_diagram}binary decision diagram} under his direction while I was a first year master student. + +{1:fake_memo Fake memoïzation} + +We provide a {!module:Memo.Fake} functor. It is useful if you want to quickly test a function you memoïzed with our {!module:Memo.Make} or {!module:Memo.MakeWeak} functor, but without memoïzing it. It'll basically do nothing and should be equivalent to your initial non-memoïzed function. + +{[ +let module Mem = Memo.Fake(struct + type t = int + let equal = (=) + let hash = Hashtbl.hash +end) + +let fibo = Mem.memo (fun fibo x -> + if x < 0 then invalid_arg "fibo"; + if x < 2 then x + else fibo (x - 1) + fibo (x - 2)) +]} + +{1:custom_cache Using your own defined cache} + +With the {!module:Memo.Mk} functor, you can also directly provide a [Cache] module, which should have the signature {!module-type:Hashtbl.S}. We will include your cache module and use it to define a [memo] function: + +{[ +let module Mem = Memo.Mk( + Hashtbl.Make(struct + type t = int + let equal = (=) + let hash = Hashtbl.hash + end) +end) + +let fibo = Mem.memo (fun fibo x -> + if x < 0 then invalid_arg "fibo"; + if x < 2 then x + else fibo (x - 1) + fibo (x - 2)) +]} + +This example is useless and equivalent to using the {!module:Memo.Make} functor directly. + +If you find a real use case for this which doesn't need new dependencies, contact me and I'll be happy to add a new functor to the library. + +It should be useful only if you want to use another {!module:Hashtbl} implementation or things like this. + +{1:tuning Tuning} + +There's a default value for the initial cache size. You can set it to the value of your choice, reset it to the default and get the current value like this: + +{[ +Memo.set_initial_cache_size 1024; +Memo.reset_initial_cache_size (); +let curr_size = Memo.get_initial_cache_size () +]} + +Note that with the current implementation of hash tables in OCaml, it's better if you choose a power of two. You may saw some code using a prime number, it's because some years ago it was the best thing to do as the hash tables implementation was different. {{:https://www.lri.fr/~filliatr/}Jean-Christophe Filliâtre} explained this to me, thanks again ! Also keep in mind that if you use your own defined cache thanks to the {!module:Memo.Mk} functor, it may not be the right thing to do. diff --git a/dune-project b/dune-project new file mode 100644 index 0000000..c320698 --- /dev/null +++ b/dune-project @@ -0,0 +1,27 @@ +(lang dune 3.0) + +(name memo) + +(license ISC) + +(authors "pena ") + +(maintainers "pena ") + +(source + (uri git://forge.kumikode.org/kumikode/memo.git)) + +(bug_reports https://forge.kumikode.org/kumikode/memo/issues) + +(homepage https://forge.kumikode.org/kumikode/memo) + +(generate_opam_files true) + +(package + (name memo) + (synopsis "Memoïzation library") + (description + "memo is an OCaml library for memoïzation. It provides easy ways to memoïze a function, in a type-safe and modular way and the ability to get forgetful memoïzation.") + (depends + (ocaml + (>= 4.12)))) diff --git a/memo.opam b/memo.opam new file mode 100644 index 0000000..a159437 --- /dev/null +++ b/memo.opam @@ -0,0 +1,30 @@ +# This file is generated by dune, edit dune-project instead +opam-version: "2.0" +synopsis: "Memoïzation library" +description: + "memo is an OCaml library for memoïzation. It provides easy ways to memoïze a function, in a type-safe and modular way and the ability to get forgetful memoïzation." +maintainer: ["pena "] +authors: ["pena "] +license: "ISC" +homepage: "https://forge.kumikode.org/kumikode/memo" +bug-reports: "https://forge.kumikode.org/kumikode/memo/issues" +depends: [ + "dune" {>= "3.0"} + "ocaml" {>= "4.12"} + "odoc" {with-doc} +] +build: [ + ["dune" "subst"] {dev} + [ + "dune" + "build" + "-p" + name + "-j" + jobs + "@install" + "@runtest" {with-test} + "@doc" {with-doc} + ] +] +dev-repo: "git://forge.kumikode.org/kumikode/memo.git" diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..ef3c6b2 --- /dev/null +++ b/shell.nix @@ -0,0 +1,14 @@ +{ pkgs ? import { } }: + +pkgs.mkShell { + nativeBuildInputs = with pkgs.ocamlPackages; [ + dune_3 + findlib + merlin + ocaml + ocamlformat + odoc + ]; + buildInputs = with pkgs.ocamlPackages; [ + ]; +} diff --git a/src/dune b/src/dune new file mode 100644 index 0000000..e357f2d --- /dev/null +++ b/src/dune @@ -0,0 +1,3 @@ +(library + (public_name memo) + (wrapped false)) diff --git a/src/memo.ml b/src/memo.ml new file mode 100644 index 0000000..eb77dbb --- /dev/null +++ b/src/memo.ml @@ -0,0 +1,106 @@ +(** *) + +(** {1:tuning Tuning} *) + +(**/**) + +let get_initial_cache_size, set_initial_cache_size, reset_initial_cache_size = + let default = 512 in + let initial_cache_size = ref default in + ( (fun () -> !initial_cache_size) + , (fun size -> initial_cache_size := size) + , fun () -> initial_cache_size := default ) + +(**/**) + +(** [mk_memo create find add ff] gives a memoïzed version of the functional [ff] + using the functions [create], [find] and [add] for the cache. It's used + internally and you shouldn't have to use it. *) +let mk_memo create find add ff = + let cache = create (get_initial_cache_size ()) in + let rec f k = + try find cache k + with Not_found -> + let v = ff f k in + add cache k v; + v + in + f + +(**/**) + +(** {1:generic Generic interface} *) + +(** [memo ff] gives you a memoïzed version of the [ff] functional. *) +let memo ff = + let open Hashtbl in + mk_memo create find add ff + +(** {1:functors Functorial interface} *) + +(** The output signature of the functors {!module:Mk}, {!module:Make}, + {!module:MakeWeak} and {!module:Fake}.*) +module type S = sig + type t + + val memo : ((t -> 'a) -> t -> 'a) -> t -> 'a +end + +(** The type of custom cache modules. *) +module type Cache = sig + (** The type of keys. *) + type key + + (** The type of caches with ['a] values. *) + type !'a t + + (** [create n] creates a new, empty cache, with initial size [n]. For best + results, [n] should be on the order of the expected number of elements + that will be in the cache. The cache must grow as needed, so [n] is just + an initial guess. *) + val create : int -> 'a t + + (** Empty a cache. *) + val clear : 'a t -> unit + + (** [add cache key v] adds a binding of [key] to [v] in cache [cache]. *) + val add : 'a t -> key -> 'a -> unit + + (** [find cache v] returns the current binding of [v] in [cache], or must + raise [Not_found] if [v] is not in [cache]. *) + val find : 'a t -> key -> 'a +end + +(** With the {!module:Mk} functor, you can also directly provide a [Cache] + module, which should have the signature [Cache]. We will include your cache + module and use it to define a [memo] function. It should be useful only if + you want to use another [Hashtbl] implementation or things like this. *) +module Mk (Cache : Cache) = struct + include Cache + + let memo ff = mk_memo Cache.create Cache.find Cache.add ff +end + +(** Functor that can be useful in case you don't want to use polymorphic + equality or you are doing things like hashconsing and you know how to + compare or hash your type more efficiently. *) +module Make (H : Hashtbl.HashedType) = + Mk [@inlined hint] (Hashtbl.Make [@inlined hint] (H)) + +(** Functor that works like the [Make] one, but the bindings in the memoïzation + cache will be weak, allowing the garbage collector to remove them if they + are not used somewhere else. *) +module MakeWeak (H : Hashtbl.HashedType) = + Mk [@inlined hint] (Ephemeron.K1.Make [@inlined hint] (H)) + +(** Functor that is useful if you want to quickly test a function you memoïzed + with our {!module:Make} or {!module:MakeWeak} functor, but without memoïzing + it. It'll basically do nothing and should be equivalent to your initial + non-memoïzed function. *) +module Fake (H : Hashtbl.HashedType) = Mk [@inlined hint] (struct + include Hashtbl.Make (H) + + let find _ _ = raise_notrace Not_found + + let add _ _ _ = () +end) diff --git a/test/dune b/test/dune new file mode 100644 index 0000000..6625db3 --- /dev/null +++ b/test/dune @@ -0,0 +1,3 @@ +(test + (name test) + (libraries memo)) diff --git a/test/test.ml b/test/test.ml new file mode 100644 index 0000000..8b8b619 --- /dev/null +++ b/test/test.ml @@ -0,0 +1,38 @@ +let _ = + let fibo_res = [| 0; 1; 1; 2; 3; 5; 8; 13; 21; 34; 55; 89 |] in + let fibo fibo x = + if x < 0 then invalid_arg "fibo"; + if x < 2 then x else fibo (x - 1) + fibo (x - 2) + in + let fibo1 = Memo.memo fibo in + let module Mem = Memo.Make (struct + type t = int + + let equal = ( = ) + + let hash = Hashtbl.hash + end) in + let fibo2 = Mem.memo fibo in + let module MemWeak = Memo.MakeWeak (struct + type t = int + + let equal = ( = ) + + let hash = Hashtbl.hash + end) in + let fibo3 = MemWeak.memo fibo in + let module MemFake = Memo.Fake (struct + type t = int + + let equal = ( = ) + + let hash = Hashtbl.hash + end) in + let fibo4 = MemFake.memo fibo in + for i = 0 to Array.length fibo_res - 1 do + assert (fibo1 i = fibo_res.(i)); + assert (fibo2 i = fibo_res.(i)); + assert (fibo3 i = fibo_res.(i)); + assert (fibo4 i = fibo_res.(i)) + done; + Format.printf "Tests are OK !@."