Skip to main content

tancore/
map.rs

1use tan::{
2    context::Context,
3    error::Error,
4    expr::{expr_clone, format_value, Expr},
5    util::{
6        args::{unpack_map_arg, unpack_map_mut_arg, unpack_stringable_arg},
7        module_util::require_module,
8    },
9};
10
11// #todo implement some of those functions: https://www.programiz.com/python-programming/methods/mapionary
12
13pub fn map_eq(args: &[Expr]) -> Result<Expr, Error> {
14    // Use macros to monomorphise functions? or can we leverage Rust's generics? per viariant? maybe with cost generics?
15    // #todo support overloading,
16    // #todo support multiple arguments.
17
18    let a = unpack_map_arg(args, 0, "a")?;
19    let b = unpack_map_arg(args, 1, "b")?;
20
21    Ok(Expr::Bool(*a == *b))
22}
23
24// #insight use `contains-key` so that `contains` refers to the value, consistent with other collections.
25// #todo consider other names: has, has-key, contains-key, includes, etc.
26// #todo consider appending a `?`
27pub fn map_contains_key(args: &[Expr]) -> Result<Expr, Error> {
28    let [map, key] = args else {
29        return Err(Error::invalid_arguments(
30            "requires `this` and `key` argument",
31            None,
32        ));
33    };
34
35    let Some(items) = map.as_map_mut() else {
36        return Err(Error::invalid_arguments(
37            "`map` argument should be a Map",
38            map.range(),
39        ));
40    };
41
42    // #todo support non-string/symbol keys
43    // #todo support string keys also.
44
45    // #todo temp solution!
46    let key = format_value(key);
47
48    // #idea instead convert key to string? or hash?
49
50    Ok(Expr::Bool(items.contains_key(&key)))
51}
52
53// #todo version that returns a new sequence
54// #todo also consider set, put
55// #todo item or element? -> I think for collections item is better.
56pub fn map_put(args: &[Expr]) -> Result<Expr, Error> {
57    let [map, key, value] = args else {
58        return Err(Error::invalid_arguments(
59            "requires `this`, `key`, and `value` arguments",
60            None,
61        ));
62    };
63
64    let Some(mut items) = map.as_map_mut() else {
65        return Err(Error::invalid_arguments(
66            "`map` argument should be a Map",
67            map.range(),
68        ));
69    };
70
71    // #todo support non-string/symbol keys
72    // #todo support string keys also.
73
74    // let Expr::KeySymbol(key) = key.unpack() else {
75    //     return Err(Error::invalid_arguments(
76    //         "`key` argument should be a KeySymbol",
77    //         key.range(),
78    //     ));
79    // };
80
81    // #todo temp solution!
82    let key = format_value(key);
83
84    // #idea instead convert key to string? or hash?
85
86    items.insert(key.clone(), value.unpack().clone()); // #todo hmm this clone!
87
88    // #todo what to return?
89    Ok(Expr::None)
90}
91
92// #todo how is this related with HTTP PATCH?
93// #todo alternative names: `merge`, `patch`, `extend` (from Rust)
94// #todo I think `extend` is better, more descriptive.
95// #todo have draining and non-draining versions (drain other.) (consuming is better than draining)
96// #todo have mutating and non-mutating versions.
97pub fn map_update_mut(args: &[Expr]) -> Result<Expr, Error> {
98    let [this, other] = args else {
99        return Err(Error::invalid_arguments(
100            "requires `this` and `other` argument",
101            None,
102        ));
103    };
104
105    let Some(mut this_items) = this.as_map_mut() else {
106        return Err(Error::invalid_arguments(
107            "`this` argument should be a Map",
108            this.range(),
109        ));
110    };
111
112    let Some(other_items) = other.as_map() else {
113        return Err(Error::invalid_arguments(
114            "`other` argument should be a Map",
115            other.range(),
116        ));
117    };
118
119    // #todo expensive clone
120    // let it = other_items.clone().into_iter();
121    // this_items.extend(it);
122
123    // #todo still expensive
124    for (key, value) in other_items.iter() {
125        this_items.insert(key.clone(), value.clone());
126    }
127
128    // #todo what to return?
129    // Ok(this.clone()) // #todo this is expensive, just use Rc/Arc everywhere.
130    Ok(Expr::None)
131}
132
133// #todo could be replaced with `some-or` or Maybe functions.
134// #todo temp method until we have Maybe
135// #todo (map :key <default>) could accept a default value.
136// #todo this should be a special form, not evaluate the default value if not needed (short-circuit).
137// #todo consider making default optional.
138pub fn map_get_or(args: &[Expr]) -> Result<Expr, Error> {
139    // #todo rename `default_value` to `fallback`, more descriptive.
140    let [map, key, default_value] = args else {
141        return Err(Error::invalid_arguments(
142            "requires `this` and `key` argument",
143            None,
144        ));
145    };
146
147    let Some(items) = map.as_map_mut() else {
148        return Err(Error::invalid_arguments(
149            "`map` argument should be a Map",
150            map.range(),
151        ));
152    };
153
154    // #todo support non-string/symbol keys
155    // #todo support string keys also.
156
157    // let Expr::KeySymbol(key) = key.unpack() else {
158    //     return Err(Error::invalid_arguments(
159    //         "`key` argument should be a KeySymbol",
160    //         key.range(),
161    //     ));
162    // };
163
164    // #todo temp solution!
165    let key = format_value(key);
166
167    // #idea instead convert key to string? or hash?
168
169    let value = items.get(&key);
170
171    // #todo can we remove the clones?
172
173    if let Some(value) = value {
174        Ok(expr_clone(value))
175    } else {
176        Ok(expr_clone(default_value))
177    }
178}
179
180// #todo Also consider the name `delete` (or even `yank`)?
181pub fn map_remove(args: &[Expr]) -> Result<Expr, Error> {
182    let mut map = unpack_map_mut_arg(args, 0, "map")?;
183    let key = unpack_stringable_arg(args, 1, "key")?;
184
185    // #todo Should return None if nothing removed!
186    // #todo Should this return the value? -> Yes make maximally useful!
187    let value = map.remove(key);
188
189    // #insight Returning the value is cheap.
190    Ok(value.unwrap_or(Expr::None))
191}
192
193// #todo consider name `keys-of` to avoid clash with variable keys? -> get-keys
194// #todo document the above in a decision file
195// #todo keys is problematic if it's in the prelude!
196pub fn map_get_keys(args: &[Expr]) -> Result<Expr, Error> {
197    let [map] = args else {
198        return Err(Error::invalid_arguments("requires `this` argument", None));
199    };
200
201    let Some(items) = map.as_map_mut() else {
202        return Err(Error::invalid_arguments(
203            "`map` argument should be a Map",
204            map.range(),
205        ));
206    };
207
208    let keys: Vec<_> = items.keys().map(Expr::string).collect();
209
210    Ok(Expr::array(keys))
211}
212
213pub fn map_get_values(args: &[Expr]) -> Result<Expr, Error> {
214    let [map] = args else {
215        return Err(Error::invalid_arguments("requires `this` argument", None));
216    };
217
218    let Some(items) = map.as_map_mut() else {
219        return Err(Error::invalid_arguments(
220            "`map` argument should be a Map",
221            map.range(),
222        ));
223    };
224
225    let keys: Vec<_> = items.values().map(expr_clone).collect();
226
227    Ok(Expr::array(keys))
228}
229
230// #todo consider other names, e.g. `items`.
231// #todo introduce entries/get-entries for other collections/containers, even Array/List.
232pub fn map_get_entries(args: &[Expr]) -> Result<Expr, Error> {
233    let [map] = args else {
234        return Err(Error::invalid_arguments("requires `this` argument", None));
235    };
236
237    let Some(items) = map.as_map_mut() else {
238        return Err(Error::invalid_arguments(
239            "`map` argument should be a Map",
240            map.range(),
241        ));
242    };
243
244    // #todo why does map return k as String?
245    // #todo wow, this is incredibly inefficient.
246    // #todo #hack temp fix we add the a `:` prefix to generate keys
247    let entries: Vec<_> = items
248        .iter()
249        .map(|(k, v)| Expr::array(vec![Expr::KeySymbol(k.clone()), expr_clone(v)]))
250        .collect();
251
252    Ok(Expr::array(entries))
253}
254
255pub fn setup_lib_map(context: &mut Context) {
256    let module = require_module("prelude", context);
257
258    // #todo add something like `get-or-init`` or `update-with-default` or `get-and-update`
259
260    module.insert_invocable("=$$Map$$Map", Expr::foreign_func(&map_eq));
261
262    // #todo add type qualifiers!
263    module.insert_invocable("contains-key?", Expr::foreign_func(&map_contains_key));
264    // #todo #deprecate Remove contains-key when all call-sites are updated.
265    module.insert_invocable("contains-key", Expr::foreign_func(&map_contains_key));
266    module.insert_invocable("put", Expr::foreign_func(&map_put));
267    module.insert_invocable("put$$Map", Expr::foreign_func(&map_put));
268    module.insert_invocable("update!", Expr::foreign_func(&map_update_mut));
269    module.insert_invocable("get-or", Expr::foreign_func(&map_get_or));
270
271    // #(Func [(Map T) Hashable] T)
272    module.insert_invocable("remove", Expr::foreign_func(&map_remove));
273
274    // #todo Remove older get-* functions {
275    module.insert_invocable("get-keys", Expr::foreign_func(&map_get_keys));
276    module.insert_invocable("get-values", Expr::foreign_func(&map_get_values));
277    module.insert_invocable("get-entries", Expr::foreign_func(&map_get_entries));
278    // }
279    module.insert_invocable("keys-of", Expr::foreign_func(&map_get_keys));
280    module.insert_invocable("values-of", Expr::foreign_func(&map_get_values));
281    module.insert_invocable("entries-of", Expr::foreign_func(&map_get_entries));
282}