Skip to content

proposal: Receiver-like function calls #58589

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

Closed
mariomac opened this issue Feb 18, 2023 · 4 comments
Closed

proposal: Receiver-like function calls #58589

mariomac opened this issue Feb 18, 2023 · 4 comments
Labels
FrozenDueToAge LanguageChange Suggested changes to the Go language Proposal v2 An incompatible library change
Milestone

Comments

@mariomac
Copy link

Goal: be able to implement method-like function calls without the implications
of methods (e.g. not defining an "implements" relation to any interface). This
new language feature would be just syntactic sugar for function
invocations, allowing to place the first argument of a function right before the
function name, so the following invocations would be exactly equivalent:

func Sum(a, b int) {
	return a+b
}

func main() {
	a, b := 3, 4
	r1 := Sum(a, b)
	r2 := a::Sum(b)
	
	fmt.Println("so", r1, "and", r2, "are the same")
}

Please observe that this proposal would require a new operator to use the first
function argument as a receiver (for, example, ::).

Related work

The idea of receiver-like function calls is already present in the Kotlin language:

https://kotlinlang.org/docs/lambdas.html#function-literals-with-receiver
https://kotlinlang.org/docs/extensions.html

They are just function calls where an argument is specified as a receiver. The main
differences with the proposal here are:

  1. Kotlin function literals with receiver have a special form of signature which
    is similar to the actual Go methods' definition. The proposal here does not introduce
    any new form of function signature.
  2. Kotlin function literals with a receiver are invoked as regular methods, using the
    dot operator, while this proposal suggests adding a different operator (for example ::)
    to differentiate the new receiver-like invocation of a function from the actual method
    invocations.

Use case: fluent concatenation of generic functions

Let's describe the following Slice[I] generic type and the Map[I, O] function
that applies a function f: I --> O to the input Slice[I] and returns a
new Slice[O], where the types I and O could differ:

type Slice[I any] []I

func Map[I, O any](is Slice[I], f func(I) O) Slice[O] {
	var os Slice[O]
	for i := range is {
		os = append(os, f(is[i]))
	}
	return os
}

If we want to perform several map operations:

	ints := Slice[int]{1, 2, 3, 4}
	halved := Map(ints, func(i int) float64 {
		return float64(i) / 2
	})
	stringed := Map(halved, func(i float64) string {
		return fmt.Sprint(i)
	})
	fmt.Println(stringed)

Because of the aspects discussed in #49085,
it is not possible to define Map as a method, so a fluent concatenation of
multiple Map invocations would require using a form like:

	fmt.Println(Map(
		Map(Slice[int]{1, 2, 3, 4},
			func(i int) float64 {
				return float64(i) / 2
			}), func(i float64) string {
			return fmt.Sprint(i)
		}))

However, using receiver-like function calls would allow us concatenate functions
in a readable manner without needing to create intermediate variables:

fmt.Println(
  Slice[int]{1, 2, 3, 4}::
    Map(func(i int) float64 {
      return float64(i) / 2
    })::
    Map(func(i float64) string {
      return fmt.Sprint(i)
    }))
@MSE99
Copy link

MSE99 commented Feb 18, 2023

A similar proposal #56283, was made and rejected before how would this solve the problems raised in that proposal?.

@mariomac
Copy link
Author

mariomac commented Feb 18, 2023

I see. From the linked, similar proposal, I understand that the remaining issue (the one which cannot be solved by using an invocation operator different than dot), is:

IDEs would need to be amended to suggest the proper functions when someone types foo ->, which right now results in nothing but would now need to look up all functions beginning with something of the foo type.

In that case, maybe a Kotlin-like explicit definition of receiver functions (not methods) would help. For example:

func (is Slice[I])::Map[I, O any](f func(I) O) Slice[O] {
	var os Slice[O]
	for i := range is {
		os = append(os, f(is[i]))
	}
	return os
}

(Notice the :: that differentiates the extension function definition from a method definition).

Defining it that way, you would prevent adding extension functions to existing types unless you define a new type containing them (just exactly as you do for adding methods to a type):

// illegal
func (i int)::Plus(o int) int { return i + o }

// legal
type Num int
func (i Num)::Plus(o Num) int { return i + o }

That would probably fit better within the Go design philosophy, not having duplicate grammars for the same action.

That restriction would also allow e.g. checking that two libraries aren't defining a method and a receiver-like functions with the same name.

That would also avoid requiring to specify the package name when invoking the function.

@Merovius
Copy link
Contributor

@mariomac ISTM that the "receiver functions" you suggest are only differentiated from methods, by a) a slight syntactic difference and b) no interface satisfaction.

In #49085, the main argument against adding methods with type parameters which do not satisfy interfaces was that it doesn't seem to clear the bar of usefulness, for mere syntactic convenience. ISTM that this proposal has the same syntactical convenience as a goal and so it has to make pretty much the same tradeoff. The tradeoff changes a little bit by shifting the downsides from "some methods don't satisfy interfaces" to "some methods look slightly different and are called receiver functions" - but that doesn't seem like a material change. And it comes at the additional cost of introducing a new token to the language.

I also personally consider the most salient argument from #56283 not to be the tab-completion issue, but this one.

@seankhliao
Copy link
Member

I don't think this is sufficiently novel compared to the recently rejected #56283 and #56242 to be worth relitigating

@seankhliao seankhliao closed this as not planned Won't fix, can't repro, duplicate, stale Feb 18, 2023
@seankhliao seankhliao added LanguageChange Suggested changes to the Go language v2 An incompatible library change labels Feb 18, 2023
@golang golang locked and limited conversation to collaborators Feb 18, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
FrozenDueToAge LanguageChange Suggested changes to the Go language Proposal v2 An incompatible library change
Projects
None yet
Development

No branches or pull requests

5 participants