You’re not far, but a couple corrections: 1. While a decent, native “compose” function would skip straight to point B, and as such would also accomplish point A, even with generics this is cumbersome.
2. Point A describes implementation of an infix pipe-operator, something implemented by nature of stack-based languages, but also in Bash, Elixir, Haskell, F#, Hack, R — a closed-alpha language sponsored by NoRedInk, Roc, even supports both left-right and right-to-left piping. Often the symbol used to signify the pipe operator is a vertical bar, with or without an angle brace. I am suggesting the “<“+”-“ symbol because it already has a meaning that is similar. Point B does bring up compositionally creating a new function, but point A is just dealing with the pipe semantics. 3. Where you write “value := (F <- G)(args…)”, you are describing the suggestion of point B, not Point A — a visible invokation must occur, per A, to clearly communicate that a function execution is occurring. The distinction only matters if Point A becomes accepted without Point B. 4. I don’t recognize your point about Go being somehow unfit for the pattern. A package written in an OOP style may benefit only rarely, but when a package is more a library of combinators and propagators, such as in ML and HPC, it is quite normal to encounter a pile of elipses, with some tokens buried in the middle. I would use the heck out of this tool of expression. 5. I fail to see the point of raising the question of calling convention. I understand that what I’m suggesting in Point C requires new behaviors on behalf of the assembler, and many more tools besides. 6. I again fail to see your point by complaining of errors. An error is a value, and that is all. If one of the functions panics, emitting the correct stacktrace would certainly be another point of cost for implementing point C, but for the example “v, e := f0 <- f1 <- f2(x, nil)”, if all of the stages can fail, an effective pipeline would be composed of stages which each short-circuit if the second argument is not nil, returning zero values and the received error. This is how one currently composes pipelines. The difference proposed by point A is that it’s much more readable. Obviously, Points B and C depend on Point A being recognized On Sat, Apr 2, 2022 at 5:26 PM Bakul Shah <ba...@iitbombay.org> wrote: > If I understand you right, you seem to want to use "<-" as the "compose" > higher order function but as an infix operator. That is, instead of "value > := F(G(args...))", you want to be able to write "value := (F <- > G)(args...)". Further, "F <- G <- H" means "F <- (G <- H)". F, G &H are > functions such that F(G(H(args...)) is a valid expression, if args... is > valid. > > My gut instinct says Go is the wrong language to extend for tacit > programming. If you look at existing code, my guess is you won't find very > many instances of code that can be mapped to tacit programming. So low > "bang for the buck". Second the optimization that you talk about in point > C. is likely not very important or even meaningful. Typically it is the > *called* function that pops the stack, not the caller and unless you can > inline things like (F<-G<-H) you can't collapse three pops into one. Third, > in Go you can't short circuit error returns. Thus if H can return an error, > you can't use it in a pipeline! You have to handle the error at the call > site of H. Ideally one (or at least *I*) would want something like > F(G(H(...)) to return to the call site of F with an erorr, without > executing F or G in case H fails. > > On Apr 1, 2022, at 3:41 PM, Sam Hughes <sam.a.hug...@gmail.com> wrote: > > Point-free programming, or "tacit programming", is a convention that > highlights the intent without syntactic noise. > > For those unfamiliar, wikipedia: > https://en.wikipedia.org/wiki/Tacit_programming > > I want better function composition. "Write a helper function to route > values out to values in, like a normal person." Sure, but who needs Go when > you have C, eh? A tacit sugaring merely provides a lexically distinctive > means to indicate the intent that function f0 should be called with the > return value of f1, and f1 of f2, and so on, with reduced syntax. > > There are a couple layers here, there's an easy win, a possible next step, > and well, madness. > > A. Go uses a thin-arrow, "<" + "-", as a receive/send operator and to > signify directionality of a channel. This same operator could be used like > so, "c := a <- b()" to assert the following: > 1. Token "a" resolves to a function pointer > 2. Token "b" returns none, one, or many values, such that "a(b())" is > legal. > 3. Token "c" can legally be assigned the value of the desugared > expression. > This suggestion imposes ambiguity between the meaning of "(chan > int)(x)<-y(z)" and "(func(int)int)(x)<-y(z)", and I have chaotic intuitions > about the meaning of "(func(int)int)(x) <- (chan int)(y)", or even > "(func(int)int)(x) <- (chan int)(y) <- z", but x(<-y) is clearly > (func[T](T)T)(x)(<-(chan[T] T)(y)). A minimally disruptive solution, then, > is to assert that the tip of such a tacit chain must be a full-syntax > invokation, e.g. for "f0 <-...f<N-1> <- ?", only "f<N>(...)" is valid. This > means expressions like "c0 <- f0 <- f1(f2 <-f3(<-c1))" are unambiguous > without a mountain of ellipses. > > B. At cost of introducing a new concept for Go entirely, it would be > convenient to declare a function as "f0 := f1 <- f2 <- f3", resolving as > suggested by the statement: "reflect.TypeOf(f0) == > reflect.FuncOf([]reflect.Type{reflect.TypeOf(f3).In(0), ..<etc>}, > []reflect.Type{reflect.TypeOf(f1).Out(0), ...<etc>})". The straightforward > path to implementation would resolve that naively, as suggested by the > following: "f0 := func[T handWaving.inT, U handWaving.outT](a... T) (...U) > {return f1(f2(f3(a...)))}". A statement like "go f0 <- f1 <-f2 <- f3", > assuming "go func[T handWaving.inT](a...T) {f0(f3(a...));}" is legal, would > be an attractive pattern. > > C. Naively, point B suggests that the functions thus concatenated could be > assembled as to preallocate the stack for the entire concatenation, > omitting the allocations and moves/copies between function calls, and > rather writing the return values to the stack for the predecessor function, > by analogy, like a reverse closure or a heterogenous recursion. For the > example "f0 := f1 <- f2 <- f3", because I expect that statement to only be > legal if "f1(f2(f3(..args..)))" is legal, the out-signature of f3 is > assignment-compatible with the in-signature of f2, and f2 to f1. Concerns > such as an element in the concatenation being recursive, blocking, or > corrupting only apply to the function at stack-head; pre-allocating even a > potentially large stack exposes only risk shared with allocating the same > stack progressively, but with lower instructional segmentation. A possible > but unlikely edge-case, if for some reason, a generic function cannot be > appropriately stenciled, the optimization being suggested might only > partially be applied, or else not applied at all. > > Ellipses are basically universal for "I give you control, but I expect it > back". Loops don't give up control, rather push it on a swing like a child. > Go-func commissions an agent, expecting no control. Point A would change > the expression somewhat, but by requiring the head of a concatenation to be > an execution, the language of "I give, but I expect" is kept, but uses the > existing language of sending on channels, "B to A, your turn A", and is > suggested as unambiguously extending the "I give, I expect" agreement > backwards towards the tail. The symbols suggested are certainly not sacred, > either. > > Point B and C are harder to recommend unequivocally, but could be potently > powerful tools for communicating very long but uncomplicated calls. Point B > has very few downsides, but represents introduction of a case where a human > might understand an expression differently than a parser, while Point C > introduces a layer of complexity to serve a use case that, for better or > worse, could serve to push a new idiom onto the Gopher community. > > These three points present no risk to the Go 1 compatibility promise, but > I will admit before anyone else that I haven't proven that Point C is worth > the investment, and I expect some difficult questions about Point B. > > -- > You received this message because you are subscribed to the Google Groups > "golang-nuts" group. > To unsubscribe from this group and stop receiving emails from it, send an > email to golang-nuts+unsubscr...@googlegroups.com. > > > To view this discussion on the web visit > https://groups.google.com/d/msgid/golang-nuts/ab4e2604-99aa-4164-a2bf-a9da29d9e0d2n%40googlegroups.com > <https://groups.google.com/d/msgid/golang-nuts/ab4e2604-99aa-4164-a2bf-a9da29d9e0d2n%40googlegroups.com?utm_medium=email&utm_source=footer> > . > > > -- You received this message because you are subscribed to the Google Groups "golang-nuts" group. To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts+unsubscr...@googlegroups.com. To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/CAAxgZyTXE3OEx_RLR6ijWHwh6Vi4jZRTJ03ahahje48e8RbPpQ%40mail.gmail.com.