💾 Archived View for theparanoidtimes.org › blog › 2018 › 12 › 14 › defns captured on 2024-06-16 at 12:18:49. Gemini links have been rewritten to link to archived content

View Raw

More Information

⬅️ Previous capture (2024-02-05)

-=-=-=-=-=-=-

🦩

Address

Unknown

the paranoid times

defns

14 Dec 2018

defns [1] is a Clojure library where I experimented with function signatures. Of course when talking about function signatures one can always mention Haskell's type signatures, but in my case the inspiration came from a relatively new language called Carp [2].

Carp is a statically typed lisp that is relatively new, it has no GC and it is being designed, among other things, for games. It is really interesting and I recommend checking it out. The place where Carp uses function/type signatures is when registering native C methods in modules. For example, this is from the language's Integer module implementation:

...
(defmodule Int
  (register MAX Int "INT_MAX")
  (register MIN Int "INT_MIN")
  (register + (λ [Int Int] Int))
  (register - (λ [Int Int] Int))
  (register * (λ [Int Int] Int))
  (register / (λ [Int Int] Int))
...

The function 'register' takes a function name and a type signature and registers the new function in the module. The type signature has the mandatory 'λ' that states that this is a function, a vector of parameter types and the return type. I found this kind of notation appealing and wanted to experiment a bit with it in Clojure. I will try to explain my train of thoughts for this project. Before that, if you want more info about Carp check the language repo.

The first thing that came to my mind is function input and output assertion using a declared type signature. When the word assertion popped up, predicates immediately came to mind. So, I needed a way to bind type declaration, predicates, and the function in a non-cumbersome way.

First decision was to make the type declaration carry the assertion information, so I used records. Each type is a record that has a name and a predicate that determines the type's validity. Like this:

(defrecord TypeSpecification [type-name predicate])

;; and a handy macro for creating them:
(defmacro deft
  [type-name predicate]
  `(def ~type-name
    (->TypeSpecification ~(name type-name) ~predicate)))

;; Here are some 'built-in' types:
(deft Fn fn?)
(deft Str string?)
(deft Int int?)
(deft Bool boolean?)
(deft Nil nil?)
(deft Key keyword?)

Comparing to Carp I decided to use a slightly different notation. Type signature is a vector instead of list and 'λ' is replaced by 'Fn' (although it could've easily been lambda). So for addition type signature would be like this:

[Fn [Int Int] Int]

Using the same philosophy, I decided to somehow bound the type declaration to the function itself. In Clojure that is easy because we have metadata. In this way each function will have metadata related to its parameters and return type already associated with it.

Lastly for the assertion itself I opted in for ':pre' and ':post' conditions. They are handy because, when present, they are used by default to check function input and output.

Now to combine everything in one macro:

(defmacro defns
  [name signature params body]
  (let [params-count (count params)]
    `(when (signature-valid? ~signature ~params-count)
       (let [[fn# p# r#] ~signature]
         (defn ~name
           {:signature ~signature
            :p p#
            :r r#
            :pretty-signature (prettify-sig ~signature)}
           ~params
           {:pre [(every? true?
                          (map (fn [f# a#] (f# a#)) (map :predicate p#) ~params))]
            :post [((:predicate r#) ~'%)]}
            ~body)))))

To explain what is going on... First we check if the signature itself is valid. If it is malformed the exception will be thrown. That part is not displayed, but you can see it in the source. If it is valid we fill out the functions metadata and ':pre' and ':post' declarations using 'defn' macro. ':pre' expression just runs each type predicate against a function parameter and checks if all of them are satisfied. ':post' runs the type predicate against the result. One catch was that ':post' expression expects that '%' symbol is untouched after macro expansion so I had to include the quote before the symbol, like this '~'%'. It looks ugly, but essentially it is a simple macro.

How to use 'defns' macro:

(use 'defns.core)

(defns add
  [Fn [Int Int] Int]
  [a b]
  (+ a b)) ; declare add function with type signature

(add 1 2) ; returns 3

(add 1 "2") ; throws assertion error

(deft PosInt (and int? pos?)) ; declare custom types

(defns pos-devide
  [Fn [PosInt PosInt] PosInt]
  [a b]
  (/ a b)) ; declare pos-devide function with type signature

(pos-devide 1 2) ; returns 1/2

(pos-devide 1 -1) ; throws assertion error

(require '[clojure.spec.alpha :as s]) ; we can use spec also

(s/def ::vec-of-int (s/coll-of int?)) ; define a spec

(deft VecInt (partial s/valid? ::vec-of-int)) ; define a custom type and make a predicate using spec

(defns add-all
  [Fn [VecInt] Int]
  [v]
  (reduce + v)) ; declare a function

(add-all [1 2 3]) ; returns 6

(add-all [1 2 '3]) ; throws assertion error

Since we store all info about type declarations in the metadata, there are a couple of useful things that can be done with it. For example function 'applies?' checks if the passed vector of parameters conforms to the type declaration, without invoking the function itself. It also checks function arity.

(applies? #'add-all [[1 2 3]]) ; returns true

(applies? #'add [1 2]) ; returns true

(applies? #'add [1]) ; returns false

The same story goes with 'valid-result?', but for result type. It also doesn't invoke the function.

(valid-result? #'add-all 1) ; returns true

(valid-result? #'add "a") ; returns false

You can extract the type signature itself:

(sig #'add-all) ; returns a vector of defns.core.TypeSpecification records

And the mandatory pretty print:

(pretty-sig #'add-all) ; returns ["Fn" ["VecInt"] "Int"]

What I found good about defns is that it provides clarity of intention in terms of what I want my function to do. Type declaration is bound to the function itself apart, for example, Clojure spec's 'fdef' which should be a separate expression. Also utilizing var metadata means that the functions carry type declaration info with it everywhere. This creates an opportunity for functions such as 'applies?' and 'valid-result?'.

What I found bad is that ':pre' and ':post' expressions are not that flexible really and you need to do some acrobatics to make it work. With that also comes the problem of error messages which, already bad, when originating from ':pre' or ':post' become even worse. Currently when assertion error is thrown, it is almost impossible to determine what is wrong. Customizing the output in ':pre' and ':post' is also impossible. One other neat thing that I think is hard to do with this approach is to define the relation between input and output values. This is possible to do with plain ':pre' and ':post' expressions and also with spec's 'fdef'. One of the ideas for future experimenting is to try to implement a new version of defns using 'fdef'.

As mentioned defns is an experiment and I haven't found an explicit purpose for it apart from that. I would love to hear any suggestions, feedback, ideas, etc. And, yes, currently the code is in bad shape. For all discussions send me an e-mail.

[1] defns

[2] Carp language

Home

--

theparanoidtimes@posteo.net

PGP

https://theparanoidtimes.org

Released under CC BY-SA

Made in 🇷🇸