-
-
Notifications
You must be signed in to change notification settings - Fork 84
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
add polymorphic parallel type #184
Conversation
Codecov Report
@@ Coverage Diff @@
## master #184 +/- ##
=====================================
Coverage 100% 100%
=====================================
Files 50 50
Lines 1092 1092
=====================================
Hits 1092 1092 Continue to review full report at Codecov.
|
I think there should be a better solution than the one proposed in the current diff. Could you expand on when array of Futures has polymorphic Rights? What is the input, desired result, encountered type error, etc? |
Basically my problem is that this gives compile error in Typescript because the Rights of the three Futures are of different type
So I have to explicitly set the polymorphic types in the array and then set the types in the next chain step:
With the changes submitted, the first case will be OK. And not only that, I don't have to write anything else. Typescript will know the types of |
I'm reluctant to overload At run-time, it is costly (in terms of performance) to enforce this constraint, which is why Fluture doesn't. However, the introduction of TypeScript allows this constraint to be enforced without a cost. Essentially, when you are using TypeScript, you are no longer able to "abuse" the By abuse I mean using it for ends it wasn't intended for. I don't oppose creativity, but I do oppose creativity at the cost of type consistency. ;) The solution you offer is to overload the
I am of the opinion that API's which use arbitrary overloading like that lead to bugs. The smaller the amount of input types for a function are, the bigger the chance that you can catch a developers mistake through type checking. In conclusion, you are simply using the wrong function to achieve your goal. Let's look at the alternatives, and consider whether they are convenient enough: The first alternative uses import {Par, seq, of as resolve} from 'fluture'
//This is how we can combine the results in sequence
resolve((user) => (list) => (id) => {...})
.ap(getUser(someUserId))
.ap(getListOfSomething(someId))
.ap(findNumberForSomething())
//If we want parallelism, like in your original example, we need Par and lift
const combineResults = lift3((user) => (list) => (id) => {...}))
seq(combineResults(
Par(getUser(someUserId)),
Par(getListOfSomething(someId)),
Par(findNumberForSomething())
)) Note that though this way of working gives us a huge amount of control, it's also pretty verbose (and your getUser(someUserId)
.both(getListOfSomething(someId))
.both(findNumberForSomething())
.chain(([[user, list], id]) => {...}) Now with this second example, the only pain point might be that we have to consume nested binary tuples. This brings me back to your original solution. You may have noticed that the signature for your function both<L, R1, R2>(Future<L, R1>, Future<L, R2>): Future<L, [R1, R2]>
function all3<L, R1, R2, R3>(Future<L, R1>, Future<L, R2>, Future<L, R3>): Future<L, [R1, R2, R3]>
function all4<L, R1, R2, R3, R4>(Future<L, R1>, Future<L, R2>, Future<L, R3>, Future<L, R4>): Future<L, [R1, R2, R3, R4]>
function all5<L, R1, R2, R3, R4, R5>(Future<L, R1>, Future<L, R2>, Future<L, R3>, Future<L, R4>, Future<L, R5>): Future<L, [R1, R2, R3, R4, R5]> This is something I would be open to merge, but it might be a lot of work, especially when you consider currying. And looking at all that work, you might say that consuming nested binary tuples isn't so bad. ;) |
That's a very interesting read, and a great insight into what you had in mind for Fluture. A few comments on what you say:
I'm not sure that's 100% true. Arrays in a polymorphic type can be cast into Tuples. There's nothing in Typescript that wouldn't allow this type casting, and so
Just like
Totally agree. Overloading feels very bad, but I wonder if that's one limitation of Typescript we have to deal with. For what I have seen, it's quite a common practice. Not only does
Reading your two solutions, I don't understand why The other solution feels far too verbose for me. I think I'm going to follow the premise that simplicity is better in every use case. I feel like I'm going to stick to |
Thank you for taking the time to understand my points of view. :)
Right, but at least TypeScript has made you aware that you are working with inconsistent types, and it gave you the chance to correct this potential mistake.
Fluture aims to provide a safer alternative, rather than a straight equivalent. Having to explicitly pass
I don't oppose using TypeScripts overloading feature to describe API using things such as currying. I wish there were a less verbose alternative, but as of yet overloading is the best we have. I do oppose the common practice of arbitrarily* overloading a function that does one thing, with a different behaviour. * By arbitrary overloading, I mean cases where functions are overloaded just because the behaviour feels similar, or the verb you used for your function happens to also relate to the second behaviour, or the function has a convenient short name, etc. Examples of this are
To me, the fact that I can cast one data type to another doesn't mean that my functions should by definition be able to operate on both. I still prefer distinguishing between functions that operate on Tuples, and functions that operate on Arrays (despite being able to cast between them, and in fact that they are the same in JavaScript). Let's look at the type signatures for the two functions (ignoring the concurrency limit for brevity): parallel :: Array (Future a b) -> Future a (Array b)
both :: (Future a b, Future a c) -> Future a (Tuple b c) They are very similar in use: parallel ([ Future.of(1), Future.of(2) ]).map(([a, b]) => a + b)
both ( Future.of(1), Future.of(2) ).map(([a, b]) => a + b) ...but their types are very different: both :: (Future a b, Future a c) -> Future a (Tuple2 b c)
all3 :: (Future a b, Future a c, Future a d) -> Future a (Tuple3 b c d)
all4 :: (Future a b, Future a c, Future a d, Future a e) -> Future a (Tuple4 b c d e)
I feel it's important to distinguish simplicity from ease of use. I feel like overloading the In my opinion, it's not worth it, which is why I haven't implemented these functions. However, your opinion might differ on the matter and I welcome a pull-request that introduces them. |
I agree with you that writing code just to avoid the overload is a lot of work for a little gain in convenience. I think it might also be a bad idea because the API would be developed in response to Typescript idiosyncrasies and not all users use Typescript. The issue this pull request tried to resolve is not present in vanilla JS anyway. I'm personally going to stick to array/tuple conversion. I know Fluture wasn't intended this way, but the compromise is worth it for me |
Current typings can't determine parallel routine types when array of Futures has polymorphic Rights.
I've followed the pattern of heterogeneous tuples, as per the PromiseConstructor in the Typescript definition for es2015 promises
I'm still wondering though if this could be improved by doing the same for Left (although the combinations may be a very high number) and/or for race.