[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)