Skip to content

Commit bb1f804

Browse files
committed
Add docs about dictionaries
1 parent 4ecd843 commit bb1f804

2 files changed

Lines changed: 222 additions & 0 deletions

File tree

_docs_src/mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ nav:
1010
- Getting Started: getting-started.md
1111
- Matplotlib Example: matplotlib.md
1212
- Matplotlib Example 2: matplotlib-2.md
13+
- Handling Dictionaries: dictionaries.md
1314
- Rules:
1415
- Types: types.md
1516
- Function & Argument Names: names.md

_docs_src/src/dictionaries.md

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
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

Comments
 (0)