Let’s say we have a function like the invoke function (check out clojure.lang.IFn) for function-like types, that is meant to invoked on an certain number of arguments whose types are not known at compile time.
;; Example of a 'function like type'; {}, an IFn that is not a function that can nontheless
;; be invoked on arguments
({:name "Cameron"
:age 100} :name) => "Cameron"
In Java, this invoke function looks something like
public Object invoke(Object arg1,Object arg2 ...)
In Rust, for now to my knowledge our two options for dynamic typing are to
- Use trait objects (combined with the Any trait specifically), something like
fn invoke<'a>(arg1: &'a dyn Any, ..) -> &'a dyn Any
or
- To have a wrapper ADT (enum) that knows all possible types ahead of time; ie, something like
fn invoke(arg1: &Value,..) -> Value enum Value { I32(i32), Symbol(Symbol), Keyword(Keyword), .. }
For now, I have moved forward with #2, as there appear to be some major issues one runs into with #1, although, like with all decisions on this sheet, I am open to hearing from others wiser than I in Rust.
It occured to me I forgot to mention a static dispatching situation like fn do_to_thing<T: Any>(thing: T)
Although I don’t think it matters, as we need heterogenous dynamic typing situations, and although that might work for a fixed argument function like
fn do_to_things<T: Any, T2: Any>(thing: T, thing2: T2)
or a fixed size data type like
Vec2<T: Any, T2: Any>(1,”cat”)
Clojure does not live in a world of fixed size anything
How best represent exceptions?
First off, I’d like to play with having flat out Conditions, as you have in something like Common Lisp, over Exceptions. This would be a difference from Clojure, so I’m not sure if this sort of divergence would require me not call this Clojure (at the same time, ‘a full on Clojure that gets to live on its own, and be all that it wants to be without inheriting the restrictions of the JVM’ is part of what I want to play with here).
There’s a few things to think about here, for now let’s just have erroneous situations flat out return a Condition type, and start adding more behavior when we get back to this.
Originally, at least ,the goal was to keep the Rust base as similar to the Java / C# codebase as possible for consistency, but now I am thinking the program may just as easily end up split up and designed completely differently.
Either way, each part in common will try to be as consistent with the original Java version as possible – and sometimes this will involve not going quite with Rust conventions, as is the example of the IFn trait, which for now is keeping the IFn name. See notes at top of IFn for more info
One glitch in my thinking that didn’t really compute when I wrote this is that the, say, Java version looks pretty straightforward underneath – if you want a hashmap, for instance, its just implemented in Java – but I am forgetting that, by virtue of the JVM, it is sort of automatically getting an efficient bytecode to evaluate to. Without that luxury, we likely do not want to just likewise implement things outright, we will likely also want to add an additional step where everything too perhaps compiles to a more efficient bytecode – or we might implement some sort of JIT action. In the long run, I’d love for this to be like SBCL, although I imagine that might take a few decades
I get the impression that runtime reflection in Rust would be difficult if not downright impossible, so for now I would like to look into producing our interop functions at compile time (or rather, ‘pre compile time’; parsing our rust files, producing the appropriate ClojureRS rust code bridging the two, and the compiling the entire project after)
At the very least, we are closer, as we have some intermediate Rust traits and types we can use to create Rust values that can live inside ClojureRS. Perhaps the next step might be some derive macros, and then generation itself might become a lot easier, as the bulk of it would be carried out from within Rust itself
Put in separate file as Value or same? Let’s keep it separate and see how its used as the program progresses
If we notice, our Value enum, which just wraps all potential types (this is how we’re implementing dynamic typing), has some ‘types’ that are not concretions but interfaces. And then it has some types that are more complicated yet, like Conditions (Conditions technically are a value with a type, but are they exactly returned in the same ‘expression space’, or aren’t they sort of returned to this separate ‘exception channel’, and not to the environment where it was written but to a handler anywhere higher up awaiting it)
Anyways, I just want to keep this in mind. Right now Value wraps everything in a flat sort of way, but they aren’t all quite on the same ‘level’.
Perhaps they could look like
(defn div [x y]
(restart-case
(if (not= y 0)
(/ x y)
;; Oh yeah this looks sexy
(error :divide-by-zero {:message "Tried to divide by zero"}))
(return-zero [] 0)
(return-value [r] r)
(div-new-vals [x y] (/ x y))
(div-new-denominator [v] (/ x v))))
(defn test-div-error
(handler-bind
:on-zero-denominator
(fn [cond]
(println (:message cond))
(invoke-restart :return-value 10))
(div 5 0)))
In Clojure proper, there is specific semantics for keywords and symbols – namely,
(intern "namespace" "name")
(intern "namespace/name")
Since Rust doesn’t have overloading, we can either do
pub fn intern(ns: Option<&str>, name: &str)
Or just have two functions. So we’ve gone with the latter
intern(name)
intern_with_ns(ns,name)
To be honest, from my time in other languages I’ve gotten used to the idea of naming all your functions, giving you all these little ‘offshoots’ of, say, intern in this case (although its basically namespacing in a way; anytime you’re doing something like blah__1 blah__other blah__3, your blah has become a ‘family name’ of sorts; a namespace).
Right now we likewise use naive immutables; the map is an O(n) associative-list, meaning its really a list of (key . value) pairs underneath, and a persistent ‘history’ is achieved by thinking of the list as a timeline of changes; the first element is the first change, the second the second, and if you hold the map as it was at this point in time, you simply point to this second entry. Then, to search for members of the map, you walk backwards through the timeline – but not forwards; things will continue to happen, but they won’t affect you because they’ll come afterwards.
The persistent vector is just a plain Vec, and more importantly I don’t think there’s any structural sharing there atm
The persistent list should be the closest to the proper Clojure structure, and is a Cons list, where likewise what sublist you reference depends on where your head is pointing to.
The plan is to look into `im` or implement the structures myself, if I find that’s necessary. I already started reverse engineering the PersistentHashMap myself, trying to come up with a structure with the same time / space complexities, although I will definitely not wait for me to figure that out – that could take weeks or years or forever – I’ll just look it up. But I’d be neato if I did
Right now, the project uses plain, also-naive reference counting. Things in the ClojureRS world live inside Rc<Values> , where Value (again) is an enum wrapping all potential types.
It will be important for this to truly grow, I think, before we start trying to observe it under the hood, mapping its activities to visuals that show us just what kind of dance is going on underneath, and where the bottlenecks are.
It will be at that time that it will be best to truly start adding a deeper design to memory management, although until then I will keep reading on what others have found before me