|
| 1 | +# Handling Python Dictionaries |
| 2 | + |
| 3 | +Let's see how to handle Python `Dictionaries`. For now, you need to define a module that has a couple of functions. For now, we will call it `Dict`. You can use a signature or `mli` file if you want, but to keep it simple, we will leave it out for now. |
| 4 | + |
| 5 | +Stick the following code in a file called `dict.ml` |
| 6 | + |
| 7 | +```ocaml |
| 8 | +type t = Pytypes.pyobject |
| 9 | +
|
| 10 | +let to_pyobject x = x |
| 11 | +let of_pyobject x = x |
| 12 | +``` |
| 13 | + |
| 14 | +Technically, that would be all you need, but it's not very easy to work with...you would have to create all your own `pyobjects` by hand. Yuck! |
| 15 | + |
| 16 | +The next thing you need is to decide what kind of interface you want your `Dict.t` to have. By that I just mean that it would be nice to have a convenient way to get standard "dictionary-like" types into `Dict.t`. In this tutorial, we will look at three: an association list, and [Base's](https://ocaml.janestreet.com/ocaml-core/latest/doc/base/Base/index.html) `Map` and `Hashtbl`. |
| 17 | + |
| 18 | +Of course, you may want to use something different, and that will work just fine after you see how to do it. |
| 19 | + |
| 20 | +## Write val specs |
| 21 | + |
| 22 | +But first we should look at the Python code we are planning to bind. |
| 23 | + |
| 24 | +`silly_map.py` |
| 25 | + |
| 26 | +```python |
| 27 | +def add(d, k, v): |
| 28 | + d[k] = v |
| 29 | + |
| 30 | +def get(d, k): |
| 31 | + return d[k] |
| 32 | +``` |
| 33 | + |
| 34 | +Just two functions to define a weird little map module: `add` and `get`, both of which take a `dictionary` as their first argument. The Python dictionary can have pretty much any types for keys and values, but we are going to use it as a `string => string` map. You should choose whatever types make sense for your particular use case. |
| 35 | + |
| 36 | +Here are the value specs to bind these functions. |
| 37 | + |
| 38 | +```ocaml |
| 39 | +val add : d:Dict.t -> k:string -> v:string -> unit -> unit |
| 40 | +val get : d:Dict.t -> k:string -> unit -> string |
| 41 | +``` |
| 42 | + |
| 43 | +## Generate bindings |
| 44 | + |
| 45 | +Now, let's generate our library code. |
| 46 | + |
| 47 | +``` |
| 48 | +$ pyml_bindgen val_specs.txt silly_map NA \ |
| 49 | + --caml-module=Silly_map -a module -r no_check \ |
| 50 | + | ocamlformat --enable - --name=x.ml \ |
| 51 | + > lib.ml |
| 52 | +``` |
| 53 | + |
| 54 | +See that weird `NA` in the command? That's because you currently have to pass in a Python class name, even if you are binding module functions. |
| 55 | + |
| 56 | +The generated OCaml module will be `Silly_map`. The other flags specify that we want to bind module associated code and not code associated with a class (`-a module`), and that we don't want to check the results of any converting code (`-r no_check`). |
| 57 | + |
| 58 | +*Note: For more info on `pyml_bindgen` CLI args, see [here](http://localhost:8000/getting-started/#running-pyml_bindgen). |
| 59 | + |
| 60 | +Here's what the generated code looks like: |
| 61 | + |
| 62 | +```ocaml |
| 63 | +module Silly_map : sig |
| 64 | + type t |
| 65 | +
|
| 66 | + val of_pyobject : Pytypes.pyobject -> t |
| 67 | +
|
| 68 | + val to_pyobject : t -> Pytypes.pyobject |
| 69 | +
|
| 70 | + val add : d:Dict.t -> k:string -> v:string -> unit -> unit |
| 71 | +
|
| 72 | + val get : d:Dict.t -> k:string -> unit -> string |
| 73 | +end = struct |
| 74 | + let filter_opt l = List.filter_map Fun.id l |
| 75 | +
|
| 76 | + let import_module () = Py.Import.import_module "silly_map" |
| 77 | +
|
| 78 | + type t = Pytypes.pyobject |
| 79 | +
|
| 80 | + let of_pyobject pyo = pyo |
| 81 | +
|
| 82 | + let to_pyobject x = x |
| 83 | +
|
| 84 | + let add ~d ~k ~v () = |
| 85 | + let callable = Py.Module.get (import_module ()) "add" in |
| 86 | + let kwargs = |
| 87 | + filter_opt |
| 88 | + [ |
| 89 | + Some ("d", Dict.to_pyobject d); |
| 90 | + Some ("k", Py.String.of_string k); |
| 91 | + Some ("v", Py.String.of_string v); |
| 92 | + ] |
| 93 | + in |
| 94 | + ignore @@ Py.Callable.to_function_with_keywords callable [||] kwargs |
| 95 | +
|
| 96 | + let get ~d ~k () = |
| 97 | + let callable = Py.Module.get (import_module ()) "get" in |
| 98 | + let kwargs = |
| 99 | + filter_opt |
| 100 | + [ Some ("d", Dict.to_pyobject d); Some ("k", Py.String.of_string k) ] |
| 101 | + in |
| 102 | + Py.String.to_string |
| 103 | + @@ Py.Callable.to_function_with_keywords callable [||] kwargs |
| 104 | +end |
| 105 | +``` |
| 106 | +## Finish the `Dict` module |
| 107 | + |
| 108 | +Okay, now that we know a little more about the Python code and our desired interface for the `Silly_map` module, let's return to the `Dict` module and fill it out. Here's the whole thing. Jump down for some explanations. |
| 109 | + |
| 110 | +```ocaml |
| 111 | +open! Base |
| 112 | +
|
| 113 | +type t = Pytypes.pyobject |
| 114 | +
|
| 115 | +let to_pyobject x = x |
| 116 | +let of_pyobject x = x |
| 117 | +
|
| 118 | +let empty () = Py.Dict.create () |
| 119 | +
|
| 120 | +let of_alist x = |
| 121 | + Py.Dict.of_bindings_map Py.String.of_string Py.String.of_string x |
| 122 | +let to_alist x = |
| 123 | + Py.Dict.to_bindings_map Py.String.to_string Py.String.to_string x |
| 124 | +
|
| 125 | +let of_map x = of_alist @@ Map.to_alist x |
| 126 | +let to_map x = Map.of_alist_exn (module String) @@ to_alist x |
| 127 | +
|
| 128 | +let of_hashtbl x = of_alist @@ Hashtbl.to_alist x |
| 129 | +let to_hashtbl x = Hashtbl.of_alist_exn (module String) @@ to_alist x |
| 130 | +
|
| 131 | +let print_endline x = |
| 132 | + Stdio.print_endline @@ Sexp.to_string_hum |
| 133 | + @@ [%sexp_of: (string * string) list] @@ to_alist x |
| 134 | +``` |
| 135 | + |
| 136 | +`of_alist` and `to_alist` let us connect the `Dict` module with association lists. |
| 137 | + |
| 138 | +The `Py.Dict.of_bindings_map` function takes two functions used to convert OCaml values to Python values, and the association list. In this case, we're passing in strings, so we use `Py.String.of_string` to convert an OCaml `string` to a `Pytypes.pyobject`. The `to_bindings_map` works in an analogous way. |
| 139 | + |
| 140 | +*Note: For more info on writing pyml bindings, check out the [py.mli](https://github.com/thierry-martinez/pyml/blob/master/py.mli) signature file.* |
| 141 | + |
| 142 | +Next, the `of/to_map` and `of/to_hashtbl` functions are pretty simple. Both `Map` and `Hashtbl` modules have a `of/to_alist` functions. So, we just call the function to convert to/from an association list, then call the matching `Dict.of/to_alist` function. |
| 143 | + |
| 144 | +Finally, I threw in a printing function that uses [sexp_of](https://github.com/janestreet/ppx_sexp_conv) to convert the `alist` to a sexp, then print it. |
| 145 | + |
| 146 | +## Setup Dune project & run |
| 147 | + |
| 148 | +Now we're ready to set up a Dune project and write a driver to run the generated code. Save these two files in the same directory in as the other files. |
| 149 | + |
| 150 | +`dune` |
| 151 | + |
| 152 | +``` |
| 153 | +(executable |
| 154 | + (name run) |
| 155 | + (libraries base pyml stdio) |
| 156 | + (preprocess (pps ppx_jane))) |
| 157 | +``` |
| 158 | + |
| 159 | +`run.ml` |
| 160 | + |
| 161 | +```ocaml |
| 162 | +open! Base |
| 163 | +open Lib |
| 164 | +open Stdio |
| 165 | +
|
| 166 | +let () = Py.initialize () |
| 167 | +
|
| 168 | +let d = Dict.empty () |
| 169 | +
|
| 170 | +let () = Silly_map.add ~d ~k:"apple" ~v:"pie" () |
| 171 | +let () = Silly_map.add ~d ~k:"is" ~v:"good" () |
| 172 | +
|
| 173 | +let () = print_endline @@ Silly_map.get ~d ~k:"apple" () |
| 174 | +let () = print_endline @@ Silly_map.get ~d ~k:"is" () |
| 175 | +
|
| 176 | +(* Another example. *) |
| 177 | +
|
| 178 | +let () = print_endline "~~~~~~~~~~~~~~~~~~~~~~~~~~" |
| 179 | +let () = |
| 180 | + print_endline |
| 181 | + @@ Silly_map.get ~d:(Dict.of_alist [ ("apple", "pie") ]) ~k:"apple" () |
| 182 | +
|
| 183 | +(* Base.Map *) |
| 184 | +
|
| 185 | +let () = print_endline "~~~~~~~~~~~~~~~~~~~~~~~~~~" |
| 186 | +let m = Map.of_alist_exn (module String) [ ("apple", "pie") ] |
| 187 | +let d = Dict.of_map m |
| 188 | +let () = Silly_map.add ~d ~k:"is" ~v:"good" () |
| 189 | +let () = Dict.print_endline d |
| 190 | +
|
| 191 | +(* Base.Hashtbl *) |
| 192 | +
|
| 193 | +let () = print_endline "~~~~~~~~~~~~~~~~~~~~~~~~~~" |
| 194 | +let ht = Hashtbl.of_alist_exn (module String) [ ("apple", "pie") ] |
| 195 | +let d = Dict.of_hashtbl ht |
| 196 | +let () = Silly_map.add ~d ~k:"is" ~v:"good" () |
| 197 | +let () = Dict.print_endline d |
| 198 | +``` |
| 199 | + |
| 200 | +Run it like so: |
| 201 | + |
| 202 | +``` |
| 203 | +$ dune exec ./run.exe |
| 204 | +``` |
| 205 | + |
| 206 | +If all goes well, you should see some zany output like this: |
| 207 | + |
| 208 | +``` |
| 209 | +pie |
| 210 | +good |
| 211 | +~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 212 | +pie |
| 213 | +~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 214 | +((apple pie) (is good)) |
| 215 | +~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 216 | +((apple pie) (is good)) |
| 217 | +``` |
| 218 | + |
| 219 | +## Wrap-up |
| 220 | + |
| 221 | +In this tutorial, we went over a couple of ways to handle Python Dictionaries. A lot of times, you will need to pass a dictionary to a Python function or return one from a Python function. Hopefully, you have a good idea of how to do this now! |
0 commit comments