Language Suggestion: Extendable pattern matching

suggestion
#1

Hi everyone, this proposal is to make possible to extend pattern matching, pretty much like how protocols work.

Introduction:

Protocols allows us to extend behaviour of types by implementing the Protocol. So you dispatch via pattern matching on a type 'T generic to something of a known type (most of the time). In other words:

Foo.t() -> String.t()
Bar.t() -> String.t()
Baz.t() -> String.t()

The idea here is to do the opposite: from a known type, it dispatch to a generic type 'T.

String.t() -> Foo.t()
String.t() -> Bar.t()
String.t() -> Baz.t()

Reasoning:

Imagine that there is a library that supports a standard on version 1.0. In standard 1.0, only three message formats are defined:

0000000, 0000001, 0000002

A current approach, would be:

def parse("0000000"), do: %Foo{}
def parse("0000001"), do: %Bar{}
def parse("0000002"), do: %Baz{}

But what if I want to support the version 1.1 of the standard, that defines 3 more message formats:

0000000, 0000001, 0000002, 0000100, 0000101, 0000102

I would need to fork the library, modify it, etc…

Solution:

I would like to propose a solution, pretty much on the same way Protocols behave: Extenders (maybe a better name, I’m bad with naming).

defextender MySuperParser do
  @doc "Parses a message"
  def parse(msg)
end

defextended MySuperParser, for: Foo do
  def parse("0000000"), do: %Foo{}
end

This doesn’t break old code, makes easier to extend pattern matching and gives us more power to transform data back and forth. The name is bad - probably someone will find a better name for this - and I think it’s a nice companion to Protocols.

1 Like
#2

You can already kinda do that with protocols using the fallback to any functionality. So the library would implement their logic on the Any implementation, and the user is free to implement any “overrides” on the more specific types’ implementations.

3 Likes
#3

How would you prevent overlapping matches? How would you ensure order of the matches? Example:

defextended MySuperParser, for: Foo do
  def parse("0000000"), do: %Foo{}
end

defextended MySuperParser, for: Bar do
  def parse("000000" <> _), do: %Bar{}
end

How would you pick the correct matcher? for: mod options doesn’t make any sense here.

1 Like
#4

Can you give an example? I cannot see how…

#5
# Library
defprotocol CodeParser do
  @fallback_to_any true
  def parse(data)
end
  
defimpl CodeParser, for: Any do
  def parse("0001"), do: Foo
  def parse("0002"), do: Bar
end

IO.inspect(CodeParser.parse("0001")) # Foo
IO.inspect(CodeParser.parse("0002")) # Bar
    
# Userland
    
defimpl CodeParser, for: BitString do
  def parse("0001"), do: Foo
  def parse("0002"), do: Bar
  def parse("0003"), do: Baz
end
    
IO.inspect(CodeParser.parse("0001")) # Foo
IO.inspect(CodeParser.parse("0002")) # Bar
IO.inspect(CodeParser.parse("0003")) # Baz

But I’d much rather suggest something like that:

# Library
defmodule ParserV1 do
  def parse("0001"), do: Foo
  def parse("0002"), do: Bar
end
    
defmodule CodeParser do
  def parse(data, parser \\ ParserV1), do: parser.parse(data)
end

IO.inspect(CodeParser.parse("0001")) # Foo
IO.inspect(CodeParser.parse("0002")) # Bar
    
# Userland
    
defmodule ParserV1_1 do
  def parse("0001"), do: Foo
  def parse("0002"), do: Bar
  def parse("0003"), do: Baz
end
    
IO.inspect(CodeParser.parse("0001", ParserV1_1)) # Foo
IO.inspect(CodeParser.parse("0002", ParserV1_1)) # Bar
IO.inspect(CodeParser.parse("0003", ParserV1_1)) # Baz
2 Likes
#6

This is soo hacky…

1 Like
#7

Yeah, this is a problem…

#8

For note, my ProtocolEx library already handles this, it handles matches and guards with all the power and cost thereof. ProtocolEx has everything from disambiguation via @priority (which I need to document), manual reconsolidation, automation reconsolidation, --verbose flag to see precisely the code that is generated, default implementation inline in the protocol or as a match-all implementation (set a super low priority then), ability to specify your implementations as defmacro's to adjust the code in the protocol body directly, ability to inline: [func1: arity1, func2: arity2, ...] the implementations directly into the protocol (able to override the generated matcher head and all), supports compile-time testing so you can absolutely verify that implementations follow your spec or it will fail compilation, and more still. All these features grew from usage and it is apparently used by quite a number of projects based on the messages I get and seems pretty solid (but issue reports and PR’s are always welcome!). :slight_smile:

And as a bonus, all the features like being able to macro/inline, set priority for matching order and all means that all the Elixir protocols that I rewrote into ProtocolEx for benchmark tests the ProtocolEx versions always outperformed (even on the notorious old-style-now-gone Access Protocol) and in some cases significantly outperformed the Elixir version. :slight_smile:

If overlapping then in mine the @priority handles that (or the implementation inclusion order otherwise, default sorted by name for identical priorities).

EDIT: Forgot that I had a thread of it here on the forums (see last posts for current usage, I only post updates, not update the initial post). ^.^