How would you idiomatically extend arithmetric functions for other datatypes in Clojure?
Asked Answered
T

3

11

So I want to use java.awt.Color for something, and I'd like to be able to write code like this:

(use 'java.awt.Color)
(= Color/BLUE (- Color/WHITE Color/RED Color/GREEN))

Looking at the core implementation of -, it talks specifically about clojure.lang.Numbers, which to me implies that there is nothing I do to 'hook' into the core implementation and extend it.

Looking around on the Internet, there seems to be two different things people do:

  • Write their own defn - function, which only knows about the data type they're interested in. To use you'd probably end up prefixing a namespace, so something like:

    (= Color/BLUE (scdf.color/- Color/WHITE Color/RED Color/GREEN))

    Or alternatively useing the namespace and use clojure.core/- when you want number math.

  • Code a special case into your - implementation that passes through to clojure.core/- when your implementation is passed a Number.

Unfortunately, I don't like either of these. The first is probably the cleanest, as the second makes the presumption that the only things you care about doing maths on is their new datatype and numbers.

I'm new to Clojure, but shouldn't we be able to use Protocols or Multimethods here, so that when people create / use custom types they can 'extend' these functions so they work seemlessly? Is there a reason that +,- etc doesn't support this? (or do they? They don't seem to from my reading of the code, but maybe I'm reading it wrong).

If I want to write my own extensions to common existing functions such as + for other datatypes, how should I do it so it plays nicely with existing functions and potentially other datatypes?

Telemark answered 12/5, 2013 at 0:16 Comment(2)
I'm also new to Clojure. Why can't you use a protocol for this?Mullock
If you read the source for core/+ it doesn't use one, so there is nothing to hook into. Check out mikera's answer though, and follow his code, you'll see how you can do it with protocols.Telemark
N
4

The probable reason for not making arithmetic operation in core based on protocols (and making them only work of numbers) is performance. A protocol implementation require an additional lookup for choosing the correct implementation of the desired function. Although from design point of view it may feel nice to have protocol based implementations and extend them whenever required, but when you have a tight loop that does these operations many times (and this is very common use case with arithmetic operations) you will start feeling the performance issues because of the additional lookup on each operation that happen at runtime.

If you have separate implementation for your own data types (ex: color/-) in their own namespace then it will be more performant due to a direct call to that function and it also make things more explicit and customizable for specific cases.

Another issue with these functions will be their variadic nature (i.e they can take any number of arguments). This is a serious issue in providing a protocol implementation as protocol extended type check only works on first parameter.

Notional answered 12/5, 2013 at 7:42 Comment(0)
C
5

It wasn't exactly designed for this, but core.matrix might be of interest to you here, for a few reasons:

  • The source code provides examples of how to use protocols to define operations that work with with various different types. For example, (+ [1 2] [3 4]) => [4 6]). It's worth studying how this is done: basically the operators are regular functions that call a protocol, and each data type provides an implementation of the protocol via extend-protocol
  • You might be interested in making java.awt.Color work as a core.matrix implementation (i.e. as a 4D RGBA vector). I did something simiilar with BufferedImage here: https://github.com/clojure-numerics/image-matrix. If you implement the basic core.matrix protocols, then you will get the whole core.matrix API to work with Color objects. Which will save you a lot of work implementing different operations.
Coelho answered 12/5, 2013 at 2:13 Comment(7)
Heh, so core.matrix was actually where my second example came from. That uses protocols? I'm going to have to re-read the code, because I didn't notice that!Telemark
Also, that doesn't really go into details around the original - implementation. So are we saying that the Clojure core implementation is hard baked and you can't extend it? What if I was trying to subtract two other types that had nothing to do with vectors? Would I follow what core.matrix does and just sit my own layer on top of core?Telemark
So I've re-read your code and I see how that works now: it's exactly how I expected the core - to work! I'm guessing it doesn't because a) it's been around awhile and b) it's slower than the direct hinted calls. I wonder if there is value in a library that just provides arithmetic alternatives that support protocols (i.e. what core.matrix does), so there is a standard way of doing this. Keep the core - fast, but allow a generic-but-slower alternative to those that want it. Maybe I'm overengineering, I've been around Java for too long...Telemark
I think you need to be clear on what "arithmetic alternatives" you are talking about and what types you intend to support. If it is for array-like structures (vectors, matrices, tensors etc.) then core.matrix already does what you want. If it is for some different constructs then there might be a scope for a new library, but I can't think of too many justifiable cases (complex numbers, quaternions maybe....?)Coelho
P.S. I think core.matrix will handle the specific Color case quite well, assuming you want to treat it as a 4D RGBA colour vector.Coelho
Yeah, you're probably right here. The obvious other genre of number would be any kind of 'typed' measurement: so length (m, cm, feet, yards) weight (kg, lb) volume (litres / fluid ounces), even money (usd, nzd) and probably lots more obscure highly domain-specific measurements. Though again, that just sounds like you'd want a generic type for "measurable" things and reuse the same code for all the examples above, so really they only count as one example :-)Telemark
(So this has been a great learning experience. I implemented vector math for Color! On the downside, Color sucks and gets upset if you have values outside an expected range, which makes math with large intermediate values impossible, so I'm back to vectors, but hey, learning!)Telemark
N
4

The probable reason for not making arithmetic operation in core based on protocols (and making them only work of numbers) is performance. A protocol implementation require an additional lookup for choosing the correct implementation of the desired function. Although from design point of view it may feel nice to have protocol based implementations and extend them whenever required, but when you have a tight loop that does these operations many times (and this is very common use case with arithmetic operations) you will start feeling the performance issues because of the additional lookup on each operation that happen at runtime.

If you have separate implementation for your own data types (ex: color/-) in their own namespace then it will be more performant due to a direct call to that function and it also make things more explicit and customizable for specific cases.

Another issue with these functions will be their variadic nature (i.e they can take any number of arguments). This is a serious issue in providing a protocol implementation as protocol extended type check only works on first parameter.

Notional answered 12/5, 2013 at 7:42 Comment(0)
C
2

You can have a look at algo.generic.arithmetic in algo.generic. It uses multimethods.

Confederate answered 11/6, 2013 at 11:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.