Funx: Reorganizing Eq and Ord
When designing Funx I leveraged protocols, which meant some concessions.
In Haskell, you can state class relationships like “every Monad is a Functor” (via superclass constraints). In Elixir, there isn’t an equivalent way to express that kind of protocol hierarchy. So instead of creating separate protocols and implying a relationship I can’t encode, I grouped the operations into a single protocol. I could split out Functor and Applicative, but in practice you are just importing functions and it’s simpler to import Monad and get map, ap, and bind.
Also, in Elixir, a protocol module can’t serve as the public entry point for functions. That’s not a problem for monads: their contexts provide namespaces: Either, Maybe, Reader. But with Eq and Ord, there aren’t obvious namespaces, so I had a choice:
- Name the protocol
Eqand put the utility functions inEq.Utils. - Name the protocol something like
Eq.Protocoland useEqas the namespace for the grab-bag of equality utilities.
I chose consistency with other protocols like Monad, Foldable, and Filterable. Having Utils in both the Ord and Eq namespaces was annoying, but manageable by aliasing them, for example as EqUtil.
Once I started building a DSL, that decision became harder to justify. The DSL couldn’t live in the Eq protocol module, which meant either explaining that the DSL lived under Eq.Utils, or introducing yet another namespace, Eq.Dsl.
At that point it was clear that in Funx, the Eq and Ord protocols mostly live in the background, which means choosing protocol naming consistency makes less sense. So I’m switching to option 2: Eq and Ord are the primary public entry points for the utility functions and the DSL, and the protocols move to Eq.Protocol and Ord.Protocol.
I considered waiting until 1.0, but once I made the decision it didn’t make sense to delay. The longer I waited, the more code I (and others) would generate that would need to be migrated.
ChangeLog
Eq module changes
-
Funx.Eq(protocol) →Funx.Eq.Protocol- The equality protocol is now
Funx.Eq.Protocol - Protocol implementations must use
defimpl Funx.Eq.Protocol, for: YourType
- The equality protocol is now
-
Funx.Eq.Utils→Funx.Eq- Utility functions moved from
Funx.Eq.UtilstoFunx.Eq - DSL merged into
Funx.Eq(no more separateFunx.Eq.Dsl) use Funx.Eqimports only theeqDSL macroalias Funx.Eqfor utility functions (optional, or use fully qualified)
- Utility functions moved from
Ord module changes
-
Funx.Ord(protocol) →Funx.Ord.Protocol- The ordering protocol is now
Funx.Ord.Protocol - Protocol implementations must use
defimpl Funx.Ord.Protocol, for: YourType
- The ordering protocol is now
-
Funx.Ord.Utils→Funx.Ord- Utility functions moved from
Funx.Ord.UtilstoFunx.Ord - DSL merged into
Funx.Ord(no more separateFunx.Ord.Dsl) use Funx.Ordimports only theordDSL macroalias Funx.Ordfor utility functions (optional, or use fully qualified)
- Utility functions moved from
Migration guide
Eq changes:
# Before
alias Funx.Eq.Utils
use Funx.Eq.Dsl # DSL macros
Utils.contramap(&(&1.age))
defimpl Funx.Eq, for: MyStruct do
def eq?(a, b), do: a.id == b.id
end
# After
use Funx.Eq # Imports eq DSL macro
alias Funx.Eq # For utility functions
Eq.contramap(&(&1.age))
defimpl Funx.Eq.Protocol, for: MyStruct do
def eq?(a, b), do: a.id == b.id
end
Ord changes:
# Before
alias Funx.Ord.Utils
use Funx.Ord.Dsl # DSL macros
Utils.contramap(&(&1.score))
defimpl Funx.Ord, for: MyStruct do
def lt?(a, b), do: a.score < b.score
end
# After
use Funx.Ord # Imports ord DSL macro
alias Funx.Ord # For utility functions
Ord.contramap(&(&1.score))
defimpl Funx.Ord.Protocol, for: MyStruct do
def lt?(a, b), do: a.score < b.score
end
Default parameter changes:
- Functions with
ord \\ Ordnow useord \\ Funx.Ord.Protocol - DSL parser defaults to
Funx.Ord.Protocolfor comparison checks
Rationale
This reorganization provides:
- Clear separation: Protocols (
*.Protocol) vs utilities (Funx.Eq,Funx.Ord) - Minimal imports:
useimports only the DSL macro, not all functions - Better discoverability: Main modules contain the utilities users interact with
- User control: Users decide whether to alias or use fully qualified names