Function equality

I could defend Q1=no and Q2=no with the following logic.

An Elm function doesn’t have to be a naked pairing of inputs to outputs. Alternately it could be treated as a specific definition of code that happens to pair inputs to outputs in a repeatable way. Sort of like an ADT opaque wrapping of a naked pairing of inputs to outputs.

If you think about what coders are after when they test for function equality, it’s not usually “will these two things offer the same output for every input”… which is lucky since that’s literally impossible to prove in a general sense, and since it fails to implementational troubles like confusing reals with IEEE 754 floats, etc.

No, usually a coder wants to know “is what’s in this step the same value I fed into the chain of functions earlier?” Or alternately, “Is the token over here the same as the one this package advertises as a default?”

So instead, let’s think of it like an ADT. Where creating a function (\x → x + 1) is really in a sense creating a wrapped object Func#17 (\x → x + 1) where the wrapper is an arbitrary unique product of where that definition was made.

Now only that object defined in that location can ever equal itself, so even x = (\x → x + 1) and y = (\x → x + 1) would set x and y as not being equal — and that could be justified by having different conceptual function wrappers due to being defined at different locations in the code.

If we do it that way then currying might not be so difficult for the compiler to support as well.
x = {conceptual ADT Func#66} (\x y → x + y)
y = x 3 {conceptually, now y = ( 3 |> Func#66 (\x y → x + y) ) or even just ( Func#66 3 ) internally}
z = x 3

and then it makes sense that y == z or y == (x (2+1)) or whatever because they are all derived from the same function defined at the same location, and then only ever curried together with equal input values.

If we were able to define function equality that way, and allow all functions that do not spring from the same definitions to be unilaterally not equal, then I think I would be comfortable with things on every level:

  • I don’t think there are any concrete uses that would be stymied
  • We’d shut down that path to runtime exception, obviously
  • We would support black box implementations where module developers could swap opaque objects out for things that contain functions without breaking calling code
  • We would avoid leaky abstractions where people might expect “mathematical” equality to translate into demonstrable equality.
  • And our concept of equality would survive currying, function nesting, and basically every real-life thing I can think of that would actually lead people to care about in what sense any functions are “equal”.

Furthermore, I don’t see how @yosteden’s point about refactoring helps here: currently “f == (identity >> f)” gives a runtime error, and I don’t see why that would be preferable to having a false value.

We shouldn’t write tests comparing functions as a shorthand for all possible input/output pairs anyway, we should just be writing good fuzz tests to directly test said input/output pairs. if g = identity >> f then we can feed as many inputs into f and g that we’d like and test to confirm their outputs match instead of trying to interrogate the function directly in a sense that is literally impossible.

But at least my proposal let’s us do things like compare functions curried from the same source with matching partial inputs, and a fair few other — predictable — transformations as well. :slight_smile: