Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Shorthand for match assignment functions #833

Closed
MatthiasJ1 opened this issue Mar 4, 2024 · 14 comments
Closed

Shorthand for match assignment functions #833

MatthiasJ1 opened this issue Mar 4, 2024 · 14 comments

Comments

@MatthiasJ1
Copy link

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.

match def neg_one_if_zero(x if x == 0)=-1
addpattern def neg_one_if_zero(x)=x

Traditional match statements do not work for assignment functions (#832):

def f(x)=
    match x:
        case 0: -1
        case _: x
                ^Coconut exiting with error: CoconutParseError

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

def f(x)
    | 0 =
        # some code
        -1
    | _ = x
@evhub
Copy link
Owner

evhub commented Mar 5, 2024

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.

@evhub evhub closed this as completed Mar 5, 2024
@MatthiasJ1
Copy link
Author

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.

@evhub evhub removed the won't fix label Mar 5, 2024
@evhub
Copy link
Owner

evhub commented Mar 5, 2024

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.

@evhub evhub reopened this Mar 5, 2024
@MatthiasJ1
Copy link
Author

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:

  1. 45% of the code representing dump_ast is addpattern def dump_ast repeated. That is a high ratio of boilerplate to logic.
  2. AFAIK there is no good way to type annotate the dump_ast argument as AST
  3. If you were to annotate the return type as 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 addpatterns, 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() = "-"

@evhub
Copy link
Owner

evhub commented Mar 7, 2024

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.

@evhub evhub added this to the v3.1.1 milestone Mar 7, 2024
@evhub
Copy link
Owner

evhub commented Mar 11, 2024

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)

evhub added a commit that referenced this issue Mar 11, 2024
Resolves   #833.
@evhub
Copy link
Owner

evhub commented Mar 11, 2024

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.

@evhub
Copy link
Owner

evhub commented Mar 12, 2024

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.

@MatthiasJ1
Copy link
Author

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?

@evhub
Copy link
Owner

evhub commented Mar 21, 2024

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

@evhub
Copy link
Owner

evhub commented Mar 23, 2024

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.overloads.

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.

evhub added a commit that referenced this issue Mar 23, 2024
@evhub evhub closed this as completed Apr 15, 2024
@evhub
Copy link
Owner

evhub commented Apr 18, 2024

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?

@evhub evhub reopened this Apr 18, 2024
@MatthiasJ1
Copy link
Author

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)

@evhub
Copy link
Owner

evhub commented Apr 26, 2024

@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.

@evhub evhub closed this as completed Apr 26, 2024
evhub added a commit that referenced this issue Apr 26, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants