Given that the draft of the KEEP I proposed got some people interested and some showed strong suppor...
n
Given that the draft of the KEEP I proposed got some people interested and some showed strong support, I decided to make a formal submission! You can find it here. I updated it with some suggestions and concerns some of you pointed out, and discussed some possible solutions. Pattern matching is a huge feature, so contributions are welcome and make sure you show your support! (or lack thereof!)
❤️ 4
👏 11
r
Great proposal Nico!
n
Thanks! It may take a long time or never happen but it's a start 🙂
s
I read through it, quickly, and it looks great!
❤️ 1
s
when doing the FP in Scala exercises with Kotlin, the feature I missed most (up until chapter 9) was pattern matching.... it's a powerful construct that can simplify code 🙂
👍 1
n
Male sure you drop an emoji :)
s
already did 😉
❤️ 1
r
I think the biggest challenge in Kotlin with pattern matching is committing to certain features that would not benefit if java brings pattern matching down the road
n
Fair point. Are you talking about being able to pattern match on Kotlin on java records? I have no idea of what a record would look like from a Kotlin interop point of view, but from a quick search they will have pretty unique bytecode, so maybe we could give them special treatment to be able to match on them?
m
I really appreciate this proposal! I had started working on the code to support this, originally wanted to have it directly in the compiler and see how hard it would be to implement. I may just hold off on attempting a compiler implementation until we see how it goes with your KEEP and with the Java proposal. In the meantime, I've also started on an implementation of simple pattern matching in Arrow Meta (pretty much like you proposed, I was inspired by Haskell/Scala). I've got a very simple plugin running as I've just started. I'm investigating how best to determine types vs values for a given pattern, and am not yet sure how far I will be able to make it using Arrow Meta. https://github.com/mattmoore/kotlin-pattern-matching-plugin
n
Glad to hear it. Make sure you chime in with an opinion ont he syntax proposals currently on the table, because that is the most unclear bit so far I'd say. I'll make sure I check out your plugin though!
r
@Nico @mattmoore if you are interested in having these as a plugin regardless of the keep I can help you build it proper. Quotes is just not gonna be enough since this requires IR and resolution interception to let the typechecker through an expression of shapes that does not understand. I would start by creating a runtimme lib that uses the unapply pattern returning a nullable and building it atop functions trying to emulate the DSL proposed in the KEEP, then seeing what the shortcommings of a commpiler free approach are and then refine it with the compiler.
There is more you can do with a compiler plugin
Such as pattern matching on types and bring types to term and value level as results which will give you a compiler computation for path dependence that can be simplified at the bytecode layer same as scala does
I mean if you go the route of a compiler plugin you can be as ambitious as you want and actually already implement exactly what is proposed in the keep so people can try it
I understand how pattern matching destructuring works in Scala and its simple to implement with a unapply based runtime but it can be taken much further since pattern matching is just a fold over any tree like structure with a list of predicates and at compile time you can make any expression lang desugar into effitient tableswitches and things that would make this successful
otherwise there is the unknown of what is the runtime cost of this abstraction and if it when java brings pattern matching there is a map on desugaring to it
m
@raulraja I'm very much interested! So after looking at this more last night (trying to think through types at term/value level) I realized my initial naive approach on doing a transformation with Meta at runtime would probably not work. I was thinking about a runtime library as well. I'm thinking (and it sounds like perhaps what you're saying, but just to clarify) some kind of a runtime lib that could even be injected at compile time that effectively replaces a
when
expression with a function call that can apply those patterns at runtime, once we have the final value/type info resolved. I'm shooting in the dark a bit, and much of this may be wrong. But I'm enthusiastic that there may be an interesting solution here. I'm talking with some of the folks in Scala contributor Gitter to get some better ideas in mind. I'd be more than happy to collaborate on this with you and @Nico ! Nico, I'll take a deeper look at your KEEP as well and see if I have any suggestions to offer. One of the things I've really missed in Kotlin is pattern matching.
n
I like the idea of writing a compiler plugin a lot. It will for sure be helpful for the keep, and at the same time we can keep it around if the proposal does not come through. I don't quite see the point of trying to do something close with a runtime lib though @raulraja, in what way is it a better alternative for the dev? It adds an extra dependency to a codebase, would probably have some extra overhead compared to a plugin, and matching would be part of any API, so there is no point in exposing it outside of compile time. Additionally, we'd be limited by the syntax we can build with a vanilla DSL.
r
it helps you understand what you need as a compiler plugin and if what you are designing requires user to use the plugin to consume it because it may require resolution at call sites
or if you are adding syntax sugar over functions or other constructs that may be easily manipulated. The first to understand what parts of the compiler your proposed syntax needs to touch is to copile it and suppress each error that it appears until it gets to analysis and gives you a type error
For example what compilation error does any of your proposed syntax show?
n
At the same time, I'd like to ask you your opinion on the possible syntax. The suggestions made by a couple people on the PR require so many keywords for tiny matches, that I am really not convinced it is the way forward. At the same time, it is hard to resolve ambiguity without them
At the moment, I get several but I think the highlight is 'Expecting ->' and 'Expecting expression' which are clearly parse errors 😕
r
there is other alternatives without the need to destructure
for exaple if you match on is and has a special modifier the right hand side gets an expression with the matched as receiver so you get direct access to the scope
and therefore already to all names destructured
n
Don't we need destructuring to match nested stuff? I can't see what what you're saying looks like atm
r
It’s similar to the Arrow Lenses DSL
n
I'll read up on it
r
Let me see if I can put together an small example
n
Amazing thanks
r
In this case the expression
Employee.company.address.street.name
is a list of composable paths that you can transform to create an optional match
since Lenses are composable unlike destructuring it allows you to compose them arbitrarirly to create coplex rules for a match
with a simple syntax
they can be projected automatically over all data and sealed classes and you have a full view of the graph and focus on any nested property
to which you just pass the function that resolves the expression if there is a match
is the same as pattern matching but without limits in composition and no need for destructuring though you can also add it
n
I see
r
so potentially a more powerful pattern matching mechanism and we can project it automatically over any type companion or arbitrary target type you want to project it on
n
So on the LHS you pass an object that would define a match and on the RHS a lambda that has all the matched stuff in scope is that right?
r
it would be like this:
Copy code
when (model) {
  matches Employee.company.address.street.name -> { foo(name) }
  matches Employee.company.name -> { bar(name) }
}
as you can see there there is no need for _ or wilcards for unmatched properties
also the second predicate ignores the fact that company has an address
and you can also match on nested collections and maps
Optics without the fuzz as a DSL is as powerful and easy as it gets for data manipulation and inspection specially for nested cases
n
Right. And how do I match a street address that begins with a capital letter for example?
Do you throw in a guard?
r
Copy code
matches Employee.company.address.street.name.let(::upperCased) -> { foo(name) }
you can add any valid expression in between
n
ohhh
r
it’s uberpowerful, has no limits in composition
and its proven by many laws
@simon.vergauwen is responsible for it
and it’s easier than anything Kotlin provides today we just haven’t pushed it because we want it as a plugin
so its automatic for all product and coproduct types in the lang, data, sealed, enumerations etc.
n
Yes I think I see how it goes now
r
but your syntax is also valid
that style of destructuring is what scala uses but it’s less powerful than optics unless you introduce the notion of destructuring objects
where you can override unapply
if you don’t then you just match in the model structure and in nested structures is extremely verbose to get to the point you want ignoring params and criptic as you have more nesting and more ignored params
you should be able to have a single destructor of one arg for a data class of say 20 args
because that may be the only thing you care about and otherwise it’d become all _
and you’ll lose count, that has been a common issue in Scala
n
I am not familiar with sala, I mainly come from Haskell. I read up on Scala though and realise the stuff I propose is just a bit of generation at the end of the day (we're calling componentN). It was suggested to destructure based on name and that would certainly solve matching on a data class with 20 things
r
yeah the issue I’m describing in Scala is Person(_, _, _, , lastName, _, id, Company(Acme, _, _, hq))
getting to some paths becomes that and there is also corner cases when you reorder properties manually and types match
since the identifiers are bound terms and not paths you can call them whatever you want
where the user was thinking it refer to
id
after a silent refactor it’s now referring to
comments
which also is a string and its bound as
id
n
Exactly. We could not come up with a nice syntax, but we could do
Copy code
is Person(company = Company(hq = hq))
With the 2nd hq being our extracted stuff
r
what if Person had more properties
is not naming them equivalent to ignoring them?
n
Person does have more properties, we are ignoring them. I am using your example
Again we could not come up with good syntax ahahah
r
Lenses solve both of those problems and also eliminates the need for copy on data classes
Are you set on the scala style?
n
I do think the scala style is what 90% of people that want pattern matching want as it looks very intuitive if you come from haskell or rust too
But we could add extras like the destructure by name
Which would save us from the problem scala suffers
r
Also is there a way to compose guards given theoritecally that is like a boolean match?
can you declare those guards outside as expressions
n
Yep I think you'll like the nested guards idea
r
?
n
Basically you can pass lambdas inside the matches do restrict the match
r
but I mean composing them as I can do know with basic boolean
r
isEmpty && whatever
n
Yep it's just a lambda so if you're matching a string we would expect you give us something String -> Boolean
r
makes sense
yeah then it’s gonna work transparently with Lenses
because we can produce those predicates already from those paths so it would fit right in too
with the syntax you propose I mean
n
Yep the quesiton was whether to use 'overall' guards only or what we called component guards
Where you can use a guard per matched element which I'm sure is what you like ebst
best*
So if you are matching a Person(String, Int) you can give me 2 lambdas one for String one for Age
Alternatively with an overall guard only one for Person
r
right but also I could create a disjoint match
Person | Zombie
and compose it to see if any given value matches?
that is what I mean the destructuring syntax is important to support also outside guards
so you can compose predicates outside the when and then use them as an expression
n
Yes, if your lambda is type as Entity -> Boolean and it goes { it is Person || it is Zombie } that's fine
r
otherwise when you have more than a few cases it becomes a long switch you can’t really refactor out because the syntax is coupled to the when construct
ok 👍
awesome thanks for clarifying Nico
n
You can define your lambda outside of the guard fine 🙂
Pass a function reference, compose it with arrow's
andThen
that all works
r
great
now the hardest question.
n
Hahah go on
r
How do you plan on managing the fact that guards denote partial functions and exhaustiveness can’t be decided any more?
n
Good one
r
everything with any simple destructuring where you ignore sommething becomes partial
and the kotlin compiler is currently not even able to find exaustiveness in generic ADTs
only sealed hierarchies
n
Guards are only part of the match. In order to check exhaustiveness you need an unconditional match without a guard 😕 unless we do some contract magic I do not see how bring guard lambdas into the exhaustiveness equation
r
you would have to compute at the type level the diff from the match and the product so you can create suggestion for the missing program with all the parts
n
And yep only exhaustiveness in sealed hierarchies the same way
when
already does
r
yeah but what I mean is that even in sealed hierarchies
you can match on partial parts
n
Ah yes ofc
There is an example like that in the proposal let me write up an example
r
so you are gonna enforce an `else`unless we compute the diff of the product and check if i t’s covered for each of the cases
n
Copy code
when (Some(1) to Some(2)) {
  is (Some(4), Some(y)) -> ...  // case 1
  is (Some(x), None) -> ...     // case 2
  is (None, Some(3)) -> ...     // case 3
  is (_, None) -> ...           // case 4
}
No guards here (again, without contract black magic I do not see how to bring them into the equation) You keep track of where an unconditional match has been performed. When all of them have you know you've exhausted all options
I do not see a problem at all nesting levels even if the implementation itself is not trivial it should be fine it's just a table at the end of the day
r
Right that for example is satisfiable by modulo and you can proof if you have covered all cases of the algebraic structure easily
but the Kotlin compiler has no notion of that
checks you are exhaustive if you cover just the types and then it smart casts the current context data flow to say the value is of that type in that path
n
Surely that can be implemented if the keep does go through? It's not rocket science just check that while building the AST
r
what I’m saying I guess is that the moment you introduce partial matches and predicates with destruction that ignore parameters you almost always are gonna have an else unless you write that exhaustive check better than it is now
yeah totally, not trying to discourage you, just that I have dealt with pattern matching and I’m now doing the same for union types and that is not fun xD
val x: A | B = a when (x) { is A -> … is B -> … }
n
Predicates that ignore parameters are a match with less conditions. At the implementation level it's a "ok, we matched everything here so all types that could have been expected here have been exhausted"
Yeah no that looks less fun ahahah
But it's the same as sealed classes when matching right
You expect to cover {A, B} and go ticking types off the list
As you match them
r
yeah, is just that that area that checks for exhaustiveness is not the easiest to deal with, but definitely doable
n
Talking from a PoV where we fork the compiler, it can't be that bad
From a PoV where we write a compiler plugin I have no idea ahahah
r
yeah with a compiler plugin it’s a bit harder depend on the part
we have enough apis in meta though to make easy most of the phases by now
n
That's very nice
You must be able to do crazy stuff
r
now yes, but when I finish hooking it up to Z3 then we’ll see
xD
n
Hahahah rip
Alright so if you have any suggestions for the 'parameter name' matching syntax or just the scala-style syntax I am more than open to suggestions
At the moment I am going to include guards in the proposal (they were no part of the deal originally) and see how people react
r
I prefer the scala one but I think most people will find the one based on named params more friendly
n
I was thinking both
If you don't include any parameter name you are just destructuring by componentN
r
is there anything special for collection style destructuring?
n
much like named arguments are optional
No, but you can easily define extension functions that will make it very haskell looking
With guards on top it should over anyone's needs
Additionally if you do named parameter matching you'll be able to match on things like the collection size
Again that's covered in the proposal though it was complained about
r
i mean how would you match on a list that has the first argument being an Int and the second a String and is a List<Any>, and you want to ignore all other elements
those types have componentN declarations
but no constructor and same for many others
they also have no named parameters since componentN does not impose a name
n
You define an extension function
Copy code
fun <T> List<T>.match2() = when(this.size) {
  0 -> Triple(null, null, null)
  1 -> Triple(get(0), null, null)
  else -> Triple(get(0), get(1), drop(2))
}

// Then use it like:

when(listOf(1,2,3,4).match2) {
  is (Int, String, _) -> ...
  is (x, y, tail) -> ...
  is (x, null) -> ...
  is (null) -> ...

}
Matches are done using
instanceOf
so it being a
List<Any>
is not an issue we are literally getting and checking
👍 1
I know it is ugly but otherwise you have to give special treatment to collections
r
that is what I ment earlier with a destructor object
in kotlin you would introduce a new operator like uninvoke
n
I know. But I understand from a dev point of view, imposing
unapply
on everyone is just a bit rude
r
it does not impose it
only uses it when you need it like in this case
n
If you use the
override
semantics it is a bit
r
where its provided by the std lib forall iterables
n
Otherwise you're stepping on `componentN`'s toes
I think having some List<T>.match1() in the stdlib would not harm anyone though but I do not think it would be popular either
r
what I mean is that that is specially treated by the compiler
so you don’t have to call .matched2()
n
But it would be an
operator fun
right
r
yes but it’s implicit
n
hum
r
never have to call it manually
and then it just takes part of the other cases
you can still use constructor based match
n
Tbh it really is not the aim of the proposal. I aimed to keep it as simple as possible and it's still huge, but you could add as a discussion possibility see what people think
👍 1
p
I was going to ask something and I see 190 comments already lol
what’s the syntax to match class fields?
Person(name = "Paco")
? or
==
or a new symbol?
what happens with the remaining fields?
it’s much easier with structs, I don’t know how scala does it
I’m familiar with ocaml, and I haven’t used it in a year
m
Hey sorry was away for a bit. I’m catching up on this though and will have some thoughts soon. Yeah I was surprised at how long the thread got lol. Actually do you all mind if we create another channel for this topic so we don’t lose these discussions in the thread? I think there will be more to discuss over time...?
Made a channel #pattern-matching
n
I am about to check out for tonight, but at the moment there is no good syntax to match on class properties. Afaik scala destructures based on
unapply
(defined by the class author) which gives you a tuple, so you extract based on position. Haskell does more or less the same without
unapply
and I propose doing the same with
componentN
. Ideally, we can also destruct by property name but again, I don't have a nice syntax for that
❤️ 2
I think another channel is good 🙂
👌🏼 2
p
In my experience what’s worked well is pattern matching syntax being the same as declaration syntax. Kotlin already has named parameters, so the match feels perfect
drop
is
for deep value matching, keep it only for type matching
👍🏻 1