Closed MatthiasJ1 closed 4 months ago
To keep things consistent for refactoring purposes, you can actually just use addpattern
even for the initial declaration. From the docs:
Additionally, addpattern def will act just like a normal match def if the function has not previously been defined, allowing for addpattern def to be used for each case rather than requiring match def for the first case and addpattern def for future cases.
Regardless, I could potentially be sold on #190, but I'm not excited about adding syntax like what you're describing; it's redundant with what already exists and doesn't seem very Pythonic to me.
doesn't seem very Pythonic to me
The whole point of this library, as seems to me, is to add a bunch of great, non-pythonic features to python from languages such as the ones you specify in the docs:
other languages that inspired Coconut include Haskell, CoffeeScript, F#, and Julia.
All of which (Coffeescript with extensions) have a syntax very similar to the one I propose here.
it's redundant with what already exists
If this is considered redundant then how would it differ from say replacing the lambda syntax with () => <expr>
. By that logic you could argue most of Coconut is redundant...? It adds syntactic sugar allowing more expressive functional programming, while also aligning the coding style with other functional programming languages. This is the same principle as your lambda syntax.
It's your language and I totally respect your freedom to veto features purely based on your opinion, but you did offer some rebuttals here which I argue contradict the rest of the direction of this project.
Hmmm... I'll keep the issue open in case anyone comes up with a syntax here that I like but currently I just don't really see the benefit of
def f(x)
| 0 =
# some code
-1
| _ = x
over
addpattern def f(0) =
# some code
-1
addpattern def f(x) = x
and in fact I think I mildly prefer the latter syntax; having the pattern explicitly in the function signature seems more clean to me.
Here is an example using the current syntax:
data Expr(value)
data Body(exprs)
data Constant(value)
data BinOp(a, op, b)
data Add()
data Sub()
AST = Expr | Body | Constant | BinOp | Add | Sub
addpattern def dump_ast(Expr(value)) = dump_ast(value)
addpattern def dump_ast(Body(exprs)) = exprs |> map$dump_ast |> "\n".join
addpattern def dump_ast(Constant(value)) = str(value)
addpattern def dump_ast(BinOp(a, op, b)) = (a,op,b) |> map$dump_ast |> " ".join
addpattern def dump_ast(Add()) = "+"
addpattern def dump_ast(Sub()) = "-"
ast = Body([
Expr(BinOp(Constant(2), Sub(), Constant(1))),
Expr(BinOp(Constant(2), Add(), Constant(3))),
])
print(dump_ast(ast))
In the above example, there are a few issues:
dump_ast
is addpattern def dump_ast
repeated. That is a high ratio of boilerplate to logic.dump_ast
argument as AST
str
, do you add the -> str
annotation to the first definition? The last? All of them? If you annotate only one of them you now need to mentally keep track of that if you ever refactor. If you have a lot of addpattern
s, you may also not see the one that has the type annotation.The suggested syntax would resolve all of these issues:
def dump_ast(node: AST) -> str
| Expr(value) = dump_ast(value)
| Body(exprs) = exprs |> map$dump_ast |> "\n".join
| Constant(value) = str(value)
| BinOp(a, op, b) = (a,op,b) |> map$dump_ast |> " ".join
| Add() = "+"
| Sub() = "-"
The repetition point is well-taken. I'll put this on the next milestone to consider what the best thing might be to do here.
Here's a first-pass proposal
case def func: <typedef>
match (<pattern>):
<body>
which would make your function
case def dump_ast: AST -> str
match(Expr(value)) = dump_ast(value)
match(Body(exprs)) = exprs |> map$dump_ast |> "\n".join
match(Constant(value)) = str(value)
match(BinOp(a, op, b)) = (a,op,b) |> map$dump_ast |> " ".join
match(Add()) = "+"
match(Sub()) = "-"
or another example
case def factorial[Num <: int | float]: (Num, Num) -> Num
"""Factorial function"""
match(0, acc=1):
return acc
match(int(n), acc=1) if n > 0:
return factorial(n - 1, acc * n)
Okay, the above syntax is working now as of coconut-develop>=3.1.0-post_dev4
(pip uninstall coconut && pip install -U coconut-develop>=3.1.0-post_dev4
), though it still needs documentation and a bit more testing before I call it done.
Biggest thing I'm currently unhappy with here is that it doesn't give you nice typing. It'd be nice if there was a way for this to translate straightforwardly into @typing.overload
declarations, but I'm not actually sure what the best way would be to do that.
I don't quite follow. Shouldn't the match sub-functions that use this syntax stay consistent with the typing specified in the function definition, i.e. dynamically determining the type would essentially take the union of the sub-function types? Or do you mean the type hinting would change based on which match statement is chosen assuming it can be statically determined?
I'd ideally like a way to separately type each case in such a way that the different signatures can be combined with typing.overload
. Currently, you often have to do something like this, and it'd be nice if this syntax would make this easier:
if TYPE_CHECKING:
import typing
@typing.overload
def my_min[T](xs: T[]) -> T = ...
@typing.overload
def my_min[T](x: T, y: T) -> T = ...
else:
addpattern def my_min([x]) = x
addpattern def my_min([x] + xs) = my_min(x, my_min(xs))
addpattern def my_min(x, y if x <= y) = x
addpattern def my_min(x, y) = y
My current best idea for this is to find a way to make this work with the protocol intersection operator (&:
):
case def my_min[T]: T[] -> T &: (T, T) -> T
match([x]) = x
match([x] + xs) = my_min(x, my_min(xs))
match(x, y if x <= y) = x
match(x, y) = y
Okay, here's a new proposal:
case def my_min[T]:
type(xs: T[]) -> T
match([x]) = x
match([x] + xs) = my_min(x, my_min(xs))
type(x: T, y: T) -> T
match(x, y if x <= y) = x
match(x, y) = y
You're allowed to have as many type
declarations as you want in the case def
, and they get compiled into typing.overload
s.
Also, one thing I can't decide is whether guards should go inside or outside the parentheses. Inside makes it closer to pattern-matching function definition, while outside makes it closer to normal case
/match
.
One last question worth considering here: should the keyword in the body to denote a pattern-matching case be match
(as in the prior examples) or case
?
I think
match def Fact:
case 0 = 1
case int(x) = x * Fact(x-1)
is more consistent with the original python syntax.
Since the first level of indentation is guaranteed to be a case rather than a statement, I think a keyword could be considered redundant:
match def Fact:
0 = 1
int(x) = x * Fact(x-1)
@MatthiasJ1 The top-level keyword has to be case
, since match def
already has an established meaning in Coconut. And the individual cases need parentheses for parsing reasons and I think should also have a keyword for clarity. I think I will change that keyword to be case
though for consistency with Python pattern-matching syntax.
A common paradigm in functional programming is a function whose implementation is determined by matching the input. This is supported in coconut by either
addpattern
functions or traditional match statements.addpattern
can be unnecessarily verbose, especially if you like descriptive function names. This also makes refactoring more annoying as now you have to rename the function multiple times.Traditional match statements do not work for assignment functions (#832):
Even if this worked, it would be overly verbose compared to the rest of Coconut which has very streamlined, terse syntax.
I propose a syntax similar to Haskell or StandardML