Clojure - dispatch on return type? (As expressive as Haskell Typeclasses)
Asked Answered
W

1

9

This is a question about the expressiveness of Clojure vs other languages such as Haskell. The broader issue is solutions to the Expression Problem

This question reached the conclusion that in general Clojure protocols (and multimethods) were less expressive than Haskell typeclasses because protocols dispatched on first argument, and Haskell typeclasses could dispatch on return type. (Now I think this reasoning is really interesting, and have no interest in starting a language war. I'm just interested in clarity of thought).

As part of breaking this reasoning down - my question is - can't we make a Clojure multimethod that dispatches on return type (or type hint). I think we can put the following expression into a Clojure multimethod:

(= java.lang.String (:tag (meta #'my-string)))

where the function is:

(defn ^String my-string [] 
  "hello world")

Edit: The point is that I can run:

(meta #'my-string)

and get the the following result without function evaluation:

{:arglists ([]), :ns #<Namespace push-price.core>, :name my-string, :column 1, 
:line 1, :file "/private/var/folders/0l/x6hr0t1j2hvcmm_sqq04vdym0000gn/T/form-
init7576840885484540032.clj", :tag java.lang.String}

ie I have some information about the intended type of my function without evaluating it.


Edit 3 (24 Apr 2014):

Suppose I have the following types: (deftype string-type [])

(deftype int-type [])

Then I have the following functions defined in terms of these types:

(defn #^{:return-type string-type} return-string [] 
  "I am returning a string")

(defn #^{:return-type int-type} return-int [] 
  42)

Now I write a function to dispatch on their return type like so:

(defn return-type-dispatch [fn-arg]
  (let [return-type (:return-type (meta fn-arg))]
    (cond 
     (= return-type string-type) 
     "This function has a return type of the string type"
     (= return-type int-type) 
     "This function has a return type of the integer type"
     :else (str "This function has a return type of:" return-type))))

Then I write a macro to run it at compile-time

(defmacro compile-time-type-check-string []
  (println (return-type-dispatch #'return-string)))

(compile-time-type-check-string)

Then I test it like so:

lein uberjar

This gives the following result:

$ lein uberjar
Compiling dispatch-type-compile-time.core
This function has a return type of:class dispatch_type_compile_time.core.string-type
...

So I appear to be dispatching on return type.

Woodworth answered 30/3, 2014 at 4:17 Comment(0)
B
9

Let's imagine there were return type polymorphism in Clojure. That would allow us to write something like the Haskell class

class Default a where def :: a

In Haskell this works because it's a compile-time error to have a fragment like

def

as it's known to be ambiguous at compile time as to what that means. In a similar vein, if we were to write the Clojure fragment

(def)

it'd be impossible to know how to dispatch that call to the proper instance. To be more clear, the evaluation order for Clojure is that that in the fragment

(foo x y z)

the expressions x, y, and z get evaluated all prior to foo. In order for (def) to work it would need to somehow examine foo (and thus force its evaluation) in order to get information about how the return value of (def) will be used.

This could be done after a CPS transformation in which case (def) would be transformed into a function like (in Haskell type notation)

class Default a where def :: (a -> r) -> r

and now we see that we could examine the continuation function in order to learn information about the type of the parameter it expects and then dispatch off of that.

Finally, given enough macro magic this could be done... but likely for now less effort than implementing a Haskell-style type system atop Clojure. Typed Clojure could be a great model for this except it's been explicitly designed so that the semantics of Clojure cannot be affected by the inferred types. This is exactly what happens in return type polymorphism and so it's explicitly impossible in Typed Clojure.

Bovid answered 30/3, 2014 at 4:31 Comment(9)
"In order for (def) to work it would need to somehow examine foo (and thus force its evaluation)" but doesn't the (meta method above explicitly get around this? You can get return information without evaluation?Woodworth
But how does the evaluation of (def), which is when you'd need to dispatch to the right return type, evaluate the meta of its calling context? That's what necessary for return type polymorphism.Bovid
I've added an example of dispatching on the right return type. Could you please amend your answer?Woodworth
What you've demonstrated isn't comparable. In technical terms you're dispatching on a return "class" not a return "type" though those terms are easily misleading. The crucial property is to think about what can be known statically versus only at runtime.Bovid
Great - thanks for the feedback. I have updated it to use return types instead of return classes. I have also demonstrated the check running at compile-time.Woodworth
While I appreciate what you're trying here, moving this into a macro does not change it from being dynamic behavior... Even if that dynamic behavior occurs at compile time. The notion of a type is more difficult to determine in presence of macros, but still sharp. If you want to go this route you could continue it by writing a preprocessor macro which rewrites code at its call site based on these static tags so that the eventual runtime code no longer has the cond in it (or any other kind of dynamic dispatch).Bovid
Perhaps another good heuristic check of types is erasability: you ought to be able to distinguish a runtime "mode" where all types have been erased but proper behavior is still guaranteed.Bovid
Would you agree that it is no longer ambiguous the meaning of def at compile-time?Woodworth
Even that is a little ambiguous. You're inserting a lot of the mechanical details which will eventually get you to how Haskell implements it's semantics (I.e. Dictionary resolution and passing) but until you get all of those details in it won't have "return type polymorphism" the way Haskell means. A better example than trying to wire all those pierces together might be to examine how type-checking works in some Haskell parser combinator libs and model that in Clojure seeing what macros are needed to get the typing to work.Bovid

© 2022 - 2024 — McMap. All rights reserved.