emmanueltouzery / prelude-ts

Functional programming, immutable collections and FP constructs for typescript and javascript
ISC License
377 stars 21 forks source link

feat(): Try monad #5

Closed davinkevin closed 6 years ago

davinkevin commented 6 years ago

Hello,

First, thanks for the work you've done. It's a great lib with many good implementations πŸ‘( message from a VAVR fan !)

But, I would like to know if you plan to integrate some concept like the Try which is very useful for some pattern of code in Javascript. (like this one)

In our case, we want to avoid some kind of code, the most common is the following :

function extract(foo) {
  if (foo && foo.bar && foo.bar.another && foo.bar.another.and && foo.bar.another.and.again) {
    return foo.bar.another.and.again;
  }
  return null;

By doing this kind of thing:

function extract(foo) {
  return Try.of(() => foo.bar.another.and.again).getOrElse(null);

Or even better, simply return the monad and let the caller decide if null is a good candidate (which is not often the case)

So, do you plan to integrate this kind of element? Are you open to PR about this evolution?

/cc @neonox31 @yann29

emmanueltouzery commented 6 years ago

Hello, thanks for the feedback, I'm happy that you like the library!

Regarding Try, you're not the first to ask, I am slowly writing a blog post on that topic, that would cover Try and Validation. But in brief, I think that Either is enough in the case of prelude.ts (for both Try and Validation), and that we don't gain anything by adding Try and Validation (because we don't have a "do notation" like haskell or "for comprehensions" like scala). However, I definitely can be wrong, and I'm interested in counter-arguments.

Actually in your case, I think Option would be a better fit than Try or Either, because you're not interested in any "failure" data.

This should work out of the box (untested), using Function1.liftOption:

// extract takes a foo, returns Option<T>
const extract = Function1.liftOption((foo:Foo) => foo.bar.another.and.again);
// extract1 takes a foo, returns T|null
const extract1 = Function1.liftOption((foo:Foo) => foo.bar.another.and.again)
    .andThen(o => o.getOrElse(null));

EDIT: take a look at Function1.liftNullable too, it may suit you better.

But otherwise it would be nicer (but potentially cannot be achieved if it's an external or a legacy API), that foo is an Option, bar is an Option field of foo, and so on. Then you could do:

return foo
    .flatMap(f => f.bar)
    .flatMap(b => b.another)
    ..
    .getOrElse(null); // or even better would be to just return the `Option`

That would be much more idiomatic I think.

And if you actually want the failure value, then you'd want a Either, you could use Function1.liftEither instead of Function1.liftOption.

So that's what we have now. I think having Try.of instead of Function1.liftEither or Function0.liftEither is probably not worth adding a new toplevel type, but I'm interested in feedback for sure. And if we keep it as it is today in prelude.ts, for sure I need to document this better, that's why I'm planning that blog post.

Let me know what you think!

emmanueltouzery commented 6 years ago

hmm getOrElse(null) doesn't work very well because it's Option<T>.getOrElse(other:T). So Option.of(5).getOrElse(null) doesn't compile (the error is Argument of type 'null' is not assignable to parameter of type 'number').

Probably I must add Option.getOrNull() like vavr has.

emmanueltouzery commented 6 years ago

I released version 0.7.4 which added also Option.getOrNull. But let me know what you think about the main point of your bug!

davinkevin commented 6 years ago

Thanks a lot for your answer.

I've used some of your answer to build this code:

const Try = (doThis) => prelude_ts.Function1.liftNullable(doThis)();

const badValue = null;
const result = Try(() => badValue.bar.another.and.again).getOrElse(5);
console.log("result", result);

const rightValue = {bar: { another: { and: { again: 12 }}}};
const another = Try(() => rightValue.bar.another.and.again).getOrElse(5);
console.log("another", another);

It's doing the job for us ! The alias Try is very usefull to simplify reading the code

Thanks for the release which add the getOrNull πŸ‘

I want your point of view about this answer, so I let the issue open. You can close it if you think this is all good

emmanueltouzery commented 6 years ago

First, just a little nitpick, for your use-case you should use Function0 instead of Function1, since you work with parameterless functions. But no big deal, and as you noticed, Function1 does the job too (that would be a compile error in typescript though).

That said, I think you make very good points that the existing lift* functions, besides not being very discoverable, are also not very concise if you have many such cases. So I've now added a couple of new functions. Here are some examples:

Option.try_(Math.random);
=> Option.of(0.49884723907769635)

Option.tryNull(()=>null);
=> Option.none()

Either.try_(Math.random, {} as string); // second parameter is needed only if you use typescript
=> Either.right(0.49884723907769635)

I didn't release yet a new version with this change, let me know what you think!

emmanueltouzery commented 6 years ago

(I picked try_ instead of try since try is a javascript keyword and Try gives the impression that there is a Try type)

davinkevin commented 6 years ago

Thanks for the tips about Function0 instead of Function1 !

And yes, modifications you've made are, for us, a great improvement. From my point of view, Option.try_ won't be very useful, because many API we use can (sadly) return null value. So, we will end up using Option.tryNull function.

Thanks for this modification !

emmanueltouzery commented 6 years ago

I'm thinking about renaming tryNull to tryNullable. I'm going to add also Option.mapNullable, which is needed for your situation and not present yet.

emmanueltouzery commented 6 years ago

So I finally released version 0.7.5 which includes the various try-like functions we discussed (and also Option.mapNullable), so I'm closing this bug. Thanks for the discussion & suggestions!

davinkevin commented 6 years ago

Thanks you πŸ’ͺ

Le jeu. 17 mai 2018 Γ  19:09, emmanueltouzery notifications@github.com a Γ©crit :

Closed #5 https://github.com/emmanueltouzery/prelude.ts/issues/5.

β€” You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/emmanueltouzery/prelude.ts/issues/5#event-1632499765, or mute the thread https://github.com/notifications/unsubscribe-auth/AB4S6jine7MUHJvDK_AA0zwrcd_xGq6Hks5tza7ggaJpZM4Tp0I9 .