[HN Gopher] Show HN: A generically typed pipe function in TypeSc...
       ___________________________________________________________________
        
       Show HN: A generically typed pipe function in TypeScript
        
       Author : upzylon
       Score  : 68 points
       Date   : 2022-08-07 16:24 UTC (6 hours ago)
        
 (HTM) web link (github.com)
 (TXT) w3m dump (github.com)
        
       | [deleted]
        
       | archarios wrote:
       | Um, Ramda has pipe and types:
       | https://www.npmjs.com/package/@types/ramda. Although I liked my
       | own implementation of a pipeP (pipe that automatically unwraps
       | promises) that let you specify the type of the input and output
       | of the pipeline: https://github.com/chughes87/ramdaP-
       | ts/blob/main/index.ts#L1....
        
         | upzylon wrote:
         | Looks like they define pipe separately for every number of
         | arguments:
         | https://github.com/DefinitelyTyped/DefinitelyTyped/blob/mast...
        
       | upzylon wrote:
       | When programming in a functional style, quite of often I find
       | myself wanting to rewrite nested function calls as a chain.
       | Hopefully at some point the proposed pipe operator will make it
       | into JavaScript and TypeScript but for now defining a pipe
       | function will have to do. The function should allow rewriting
       | something like                 double(square(half(2)))
       | 
       | as                 pipe(half, square, double)(2)
       | 
       | For a while now I've struggled to implement the type definition
       | for that function, so that every passed-in function can only
       | accept the return type of the previous function as its parameter
       | and the resulting function will take the arguments of the first
       | function as its parameters and return the type of the last
       | function.
       | 
       | The main problem with this is that trying to define a type for
       | pipe's arguments up-front would require typing them as an
       | infinitely-recursive type that TypeScript cannot handle. A common
       | workaround for this is to define pipe's type separately for every
       | number of arguments it can take. This is for example how RXJS
       | defines its pipe function:
       | https://github.com/ReactiveX/rxjs/blob/f174d38554d404f21f98a...
       | 
       | Other common solutions are to make concessions like requiring all
       | functions to have the same return type or letting pipe only take
       | one function at a time and returning an object with methods to
       | add another function and invoke the chain. None of these
       | solutions are satisfying in my opinion.
       | 
       | I think I've finally found an implementation that fulfills all
       | these criteria. The argument and return types of passed in
       | functions are correctly enforced (no matter the number of passed-
       | in functions) and the pipe function returns a function that
       | accepts the arguments of the first function, invokes all
       | functions in turn with the result of the last, and correctly
       | returns the type of the last function of the chain. Including
       | asynchronous functions in the chain works to: if a function
       | returns a promise that promise is resolved before being passed
       | into the next function and the function returned from pipe will
       | return a promise as its type.
       | 
       | There is one disadvantage to the implementation that I'm aware
       | of: When passing in anonymous functions, the types of their
       | arguments can not be inferred if they aren't annotated. That
       | means that                 pipe(() => 10, n => n.toString())
       | 
       | would return the type                 () => any
       | 
       | but I think that's an acceptable tradeoff because when annotated
       | pipe(() => 10, (n: number) => n.toString())
       | 
       | it will return the correct type                 () => string
       | 
       | Thought the implementation might be worth sharing here in case
       | it's useful to someone else and because it's an interesting
       | problem to solve in TypeScript's type system.
       | 
       | If you have any suggestions on how to improve the function's
       | typing or know of any better implementations, I'd appreciate it
       | if you would let me know!
       | 
       |  _Edit_ : I meant to link to the pipe part of the readme, but I
       | see the link is just to the repo, that's unfortunate.
       | 
       | Here is the relevant section of the readme:
       | https://github.com/MathisBullinger/froebel#pipe
       | 
       | and here the implementation:
       | https://github.com/MathisBullinger/froebel/blob/main/pipe.ts...
        
         | [deleted]
        
         | [deleted]
        
         | aabbcc1241 wrote:
         | An easier alternative is to wrap the value into an array, then
         | use `.map()` for each function in the chain, and finally escape
         | the value with `[0]`
         | 
         | I made a similar data structure[1] to allow adding side effect
         | (no return value) as part of the chained function.
         | 
         | [1]
         | https://github.com/beenotung/tslib/blob/9f9a9274c1e13be7ba83...
        
         | brundolf wrote:
         | This is a very elegant solution that some languages support
         | (including the one I'm working on :) ):
         | https://en.wikipedia.org/wiki/Uniform_Function_Call_Syntax
         | 
         | One downside can be namespace pollution, but imo it's worth it
         | a lot of the time
        
       | cercatrova wrote:
       | What do people think of fp-ts?
        
         | l2cluster wrote:
         | I really like it, but TypeScript syntax is a bit unreadable
         | with it. I'm used to Scala which looks a lot cleaner with
         | extension methods and collections being immutable by default.
        
         | upzylon wrote:
         | That looks interesting, I hadn't heard of the project. Will
         | definitely have a look at the library.
         | 
         | But it defines its pipe function the same way RXJS does,
         | separately for any number of arguments:
         | function pipe<A>(a: A): A       function pipe<A, B>(a: A, ab:
         | (a: A) => B): B       function pipe<A, B, C>(a: A, ab: (a: A)
         | => B, bc: (b: B) => C): C       function pipe<A, B, C, D>(a: A,
         | ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D): D       ...
         | 
         | https://github.com/gcanti/fp-ts/blob/master/src/function.ts#...
        
           | jackblemming wrote:
           | > But it defines its pipe function the same way RXJS does,
           | separately for any number of arguments
           | 
           | What's the issue? This has been a common way to do this sort
           | of thing since C++ template meta programming. Have you
           | honestly ever broke the bounds?
        
             | upzylon wrote:
             | There isn't necessarily any issue with it besides maybe not
             | being very maintainable. I just tried to find a generic
             | solution for the fun of it
        
         | homami wrote:
         | I fully recommend this. The fp-ts is quite readable for
         | JavaScript. Combine it with io-ts and you have a robust way of
         | handling user inputs:                   pipe(
         | decoder.decode(req.body)           , E.mapLeft(_ex => new
         | InputValidationError(decoder.decode(req.body)))           ,
         | RTE.fromEither           , RTE.chain(handleInput)           ,
         | RTE.match(             error => {
         | res.status(500);               res.send(error)             },
         | result =>  {               res.send(result)             }
         | )         )(reader)
        
       | btown wrote:
       | This is quite elegant!
       | 
       | Direct link to the source of OP's pipe function and recursive
       | type definitions:
       | https://github.com/MathisBullinger/froebel/blob/main/pipe.ts...
       | 
       | On a related note, I've been frustrated by the slow progress on
       | https://github.com/tc39/proposal-pipeline-operator - specifically
       | the thread in https://github.com/tc39/proposal-pipeline-
       | operator/issues/91 which has 668 comments over 4+ years and shows
       | no meaningful sign of consensus.
       | 
       | The TC39 group is (justifiably) very concerned about backwards
       | and future compatibility, and Typescript has a policy of not
       | introducing syntax that is in scope for Javascript itself until
       | the syntax has formally reached a stable state (see:
       | https://github.com/Microsoft/TypeScript/issues/2103#issuecom...)
       | - so we're far from having a pure operator for this.
       | 
       | But `pipe((n: number) => n.toString(), (a: string) => a+' ')(3)`
       | is as clean as I've ever seen it get.
       | 
       | And to use it in an ad-hoc way for left-to-right readability (and
       | for code that will be maintained by those who think curry is just
       | a tasty dish), it's trivial to implement an applyPipe on top:
       | applyPipe('foo', strip, title, (s: string) => `${s}: bar`)
       | 
       | OP - it would be great to have that, or something named slightly
       | better, out of the box!
        
         | ledauphin wrote:
         | in the Python library `returns`, `applyPipe` is called `flow`,
         | and I agree that it's a very handy utility to have on top of a
         | pipe.
        
         | upzylon wrote:
         | Thanks for linking the code, I meant to link to the pipe
         | section of the README but obviously botched that and can't
         | change it now :)
         | 
         | I was really exited about the pipe proposal and actively
         | followed the discussion like 4 years ago. But with every
         | passing year of no progress my excitement is slowly dying.
         | 
         | An `applyPipe` function is an interesting idea. My only concern
         | with it is that the first function in the pipe could have more
         | (or less) than one argument. And in that case how would you
         | know where the arguments stop and the functions begin? Maybe
         | the first parameter should be an array of the arguments. Or
         | applyPipe could be of the form applyPipe(1, 2)(add, square,
         | whatever). What do you think? If there is interest in this I'll
         | add it.
        
           | btown wrote:
           | If I'm reaching for this form, I'm likely thinking about a
           | pipeline of unary functions, and the TC39 proposal seems to
           | take the same approach. Plus, doesn't every other function in
           | the pipeline need to be unary after the first?
           | 
           | As a half-baked thought, perhaps callPipe would take exactly
           | one non-function argument to start the pipeline (which would
           | appeal to most use cases), and applyPipe could take an array
           | of args, mimicking the call/apply duality in native JS?
        
             | upzylon wrote:
             | Yes, every function except the first one needs to have
             | exactly one argument.
             | 
             | Others have also said that `pipe` is not an ideal name for
             | the function. So perhaps renaming it to `compose` and have
             | `pipe` be of the form `pipe(arg, funA, funB, ...)` (like
             | you suggested for `applyPipe`) might be the solution. Will
             | need to think about it a bit more.
        
               | anderskaseorg wrote:
               | Note 'compose' conventionally connotes a right-to-left
               | order, so that (f [?] g [?] h)(x) = f(g(h(x))).
        
         | dan-robertson wrote:
         | Is there an implementation where 'then' is added to
         | Object.prototype as:                 function(f, ...preargs) {
         | return f(...preargs, this) }
         | 
         | And then you can use it to pipeline sync and async things
         | together? It's still kinda bad and seems pretty crazy though.
         | Some obvious issues are:
         | 
         | - the preargs thing presumably doesn't work for promises so
         | you'd need to use bind.
         | 
         | - there's no great way to specify 'the function which is
         | property foo of the object being operated on' to e.g. map an
         | array that is in a promise you can't write .then(.map, ...).
         | But I think this is also a problem with other pipe
         | implementations.
        
       | semicolon_storm wrote:
       | Is this what modern idiomatic TS looks like?
       | 
       | Reading through the pipe implementation, as someone who primary
       | works in the relatively "boring" world of backend languages, this
       | looks like the kind of code that would get shot down review for
       | being too clever and not very readable, regardless of how
       | "elegant" it may be.
       | 
       | Using triple assignments (a = b = somevalue)? Using l as a type
       | name instead of writing out lambda with symbols that are actually
       | easy to type? Overloading/reusing variables that makes it hard to
       | mentally trace how data flows through?
        
         | jackblemming wrote:
         | Why does it matter how complicated it is if you're not the one
         | maintaining it? Your head would probably spin if you saw how
         | complicated the database code you use is, as a backend dev.
        
           | semicolon_storm wrote:
           | Some domains require horribly complicated code that I'm
           | perfectly happy admitting that I'll never understand it.
           | Chaining function calls is not one of those domains.
           | 
           | I suppose I don't really care, it's not my library and I'll
           | never use it, it's just interesting to see how differently
           | styled it is.
        
         | upzylon wrote:
         | Chaining assignments and using non-ascii characters as variable
         | names is definitely not the norm in most TypeScript projects
         | I've seen and worked on.
         | 
         | When I started this project it was just for my own personal use
         | and kind of a creative / intellectual outlet for me where I
         | didn't have to make any compromises and adhere to anyone else's
         | style. So to this day some of the code might be a bit...
         | exotic. It's definitely not how I'd write it in my day job.
         | 
         | That's the JS part. The type-definitions I'm afraid are just
         | unreadable because it's TypeScript. I feel they're like regexes
         | in that regard: write once and try not to touch them again.
        
         | slaymaker1907 wrote:
         | The alternative is like 20 different type overloads and even
         | then, it wouldn't be fully general.
        
       | teaearlgraycold wrote:
       | Only complaint here is on the function signature. Normally a pipe
       | operator takes in arguments first and then sequential functions.
       | This pipe function fixes the reverse ordering of nested function
       | calls, but the starting inputs will always appear at the end.
       | 
       | You've gone from:                   fourth(third(second(first)))
       | 
       | to                   pipe(second, third, fourth)(first)
       | 
       | when you could have done                   pipe(first, second,
       | third, fourth)
        
         | sfvisser wrote:
         | Yes, more akin to what you'd normally call 'compose'. Fun stuff
         | nonetheless!
        
           | mo_po2 wrote:
           | I agree. Calling it pipe adds confusion to FL understanding
           | for newcomers.
        
       | slaymaker1907 wrote:
       | Hmm, I didn't think it was actually possible to write a general
       | and generic type for a function like pipe/compose. It's something
       | that many statically typed languages struggle with unless they
       | bring in the 500lb gorilla that is a full dependent type system.
        
         | brundolf wrote:
         | I don't know the definition of "dependent type system" with
         | enough precision to know whether or not TypeScript technically
         | counts as one, but it has lots of powerful capabilities that
         | allow driving the type system via provided values
        
       | Ideabile wrote:
       | This is great, thanks for sharing.
       | 
       | I'm currently using @arrows/composition
       | https://caderek.github.io/arrows/packages/composition/#pipe
       | because of the light way library approach, which you seem to
       | share.
       | 
       | I also try fp-ts https://github.com/gcanti/fp-ts but is a bit an
       | academic style, so difficult to introduce in everyday work.
       | Implementation of pipe: https://gcanti.github.io/fp-
       | ts/modules/function.ts.html#pipe
       | 
       | I still need to wrap my head on your use of generics, but yours
       | looks more flexible than the static type approach that other
       | libraries (include RxJS) implements, does your pipe support types
       | for any length of arguments? Does require a specific version of
       | TypeScript?
       | 
       | Nice work.
        
         | upzylon wrote:
         | Thanks! Yes, it should work with any number of arguments. Or at
         | least in theory it does, it seems that around 47 chained
         | functions TypeScript will give up and stops computing the type.
         | It's written in TypeScript v4.7.2 but I didn't use any recently
         | introduced features, so it should also work a few version back.
         | How far back exactly, I'd need to check.
        
       | rockyj wrote:
       | I had built an "async" version of this -
       | https://github.com/rocky-jaiswal/async-utils/blob/main/src/p...
       | 
       | Basically can solve a lot of problems by passing state to a list
       | of "piped" functions which modify the state and eventually
       | generate an output.
        
       | vorticalbox wrote:
       | My issue is for things like select you would want to take you
       | path first then your data last, this let's you build functions
       | like
       | 
       | const selectFoo= select(['foo']);
       | 
       | selectFoo({foo:'bar'})
       | 
       | But also then allows these functions to be used in pipes
       | 
       | const upperFoo = pipe( selectFoo, toUpper )
       | 
       | This is how the ramda library does it[0]
       | 
       | [0] https://github.com/ramda/ramda
        
       ___________________________________________________________________
       (page generated 2022-08-07 23:00 UTC)