Skip to content
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

How to unwrap Monads for return statements that expect a value? #499

Open
JustinHoyt opened this issue Oct 7, 2020 · 11 comments
Open

How to unwrap Monads for return statements that expect a value? #499

JustinHoyt opened this issue Oct 7, 2020 · 11 comments

Comments

@JustinHoyt
Copy link

Is your feature request related to a problem? Please describe.
In my use case I would like to use functional programming techniques, using ramda for a functional library and Crocks for an algebraic data structure library to write code in a not fully functional codebase. No one else on my team has any familiarity with Functional Programming and I'm new to it myself. I want to be able to pull values out of monads so I can return normal values in a codebase that's not functional in nature and slowly add more functional elements. I'm typically going to be using Either, IO, and Maybe monads to write my code, then extract the final result out of the resulting monad so I can return the value to a function that is not made to accept monads yet.

Describe the solution you'd like
Folktale has a function called getOrElse which will return a value or an undefined/error string. This is super useful and allows me to write functionally in an environment that does not expect to handle monads. For example, I can make some Results/Either monads, chain them together and then carefully unwrap the value for possible return. I get some functioal benefits with a small amount of impurity at the end of my function. Does Crocks have something similar or is there another way I can unwrap Either, IO, or Maybe with an internal function?

Describe alternatives for how you do this now
My current workaround is just to use Folktale, but the maintainer has mentioned that they don't have the bandwidth to maintain the project anymore. Folktale also has a limited amount of monads for me to use.

Code

const simpleFunction = (a) => {
    myMaybe = Maybe.of(a);

    // some random transformations on myMaybe

    // this will fall back to the second case if the maybe is empty
    return myMaybe.getOrElse() || doSomethingElseOnError();
}

Additional context
I also made a stackoverflow with this question, but could not tag it properly because I don't have enough reputation to add a Crocks tag.

Thanks in advance for your advice or feedback!

@dalefrancis88
Copy link
Collaborator

dalefrancis88 commented Oct 7, 2020

Check out the example here, https://crocks.dev/docs/crocks/Result.html#either or https://crocks.dev/docs/crocks/Maybe.html#either I think it's the closest to what you're trying to do.

What you're essentially touching on is folding out the value. In this scenario you need to ensure that you can take a Maybe a and pass in two functions that are () -> b and a -> b where b is the return value of your function that does not want to return a Monad

@JustinHoyt
Copy link
Author

This is exactly what I'm looking for! My understanding is that IO monads don't allow for an unfolding function like this, right? In that case would you suggest I wrap getting IO values by throwing them immediately into a Result?

Also, if you want free internet points, you can paste what you wrote into my stackoverflow question I'll accept the answer.

@evilsoft
Copy link
Owner

evilsoft commented Oct 7, 2020

@JustinHoyt So these type of functions have a really cool name called cata-morphisms you can think of them as ways to fold out your result. Most Sum Types (Either, Result, Maybe) have an either that can fold them out. Also you can think of reduce on Array as one of these functions, that move out of the type in a way that let you take into account the effects.

For Exponential Types, Like Reader and IO, their cata-morphisms are things like run (for IO) and runWith. These both apply the effects at some edge. It is easier to use things like IO when you have a clear edge to run your effects. If possible I would try to go the other way of putting your Result inside your IO like IO (Result e a) then do all your Result stuff in the IO by chaining it in. That way when you do run at your edge, you will get your Result back.

If you run an IO at some place in your code, it can be hard to deal with multiple edge within a flow. If you provide some arbitrary requirements, I should be able to give you an example of how to "flip the types".

@JustinHoyt
Copy link
Author

Here is an example of something simple I'm trying to do to lift a value out of an IO(Result e a)

const R = require('ramda');
const { Result, IO } = require('crocks');
const { Err, Ok } = Result;

const first = IO.of(() => process.argv[2] ? Ok(process.argv[2]) : Err('first arg invalid'));
const second = IO.of(() => process.argv[3] ? Ok(process.argv[3]) : Err('second arg invalid'));

const mAdd = R.liftN(2, R.add);
const result = mAdd(chain(first), map(second));

console.log(IO.run(result));

I am running into two issues:

  • I believe lift only lifts an N-arity list of params up one level, not two so I need to map over a lift function which feels weird.
  • result.run() and IO.run() don't seem to exist. I'm probably getting the wrong result, but it's not clear to me how to use IO.run. I don't see anything in the docs that mention it besides here.

@evilsoft
Copy link
Owner

evilsoft commented Oct 8, 2020

@JustinHoyt very nice. So a here are a couple of pointers to get you started.

  • We get the most out of these types when they are separate and are "stitched" together to combine their effects/embellishments. Take for instance the creation of the Result inside of the IO, this will limit ALL interactions with the process arguments to the Result
  • lift functions usually take instances of Applicatives as opposed to functions. You can use things like fanout/merge or converge/compose2 for pushing the results of functions into the lift.
  • As there are two effects here, we need to lift a lift so that both effects are applied. Kinda like doing map(map(fn)) like we do for Functor composition, but instead of covarient Functors, these are Applicative Functors.
  • The run function is on the instance, as opposed to the TypeRep, so it would have been result.run()

With those points in mind, i came up with something like this:

const IO = require('crocks/IO')
const Result = require('crocks/Result')

const add = require('ramda/src/add')
const compose = require('crocks/helpers/compose')
const curry = require('crocks/helpers/curry')
const constant = require('crocks/combinators/constant')
const ifElse = require('crocks/logic/ifElse')
const isNumber = require('crocks/predicates/isNumber')
const liftA2 = require('crocks/helpers/liftA2')

const { Err, Ok } = Result

const validResult = curry(
  (pred, err) => ifElse(pred, Ok, compose(Err, Array.of, constant(err)))
)

const isValid =
  validResult(isNumber)

const first =
  IO(() => process.argv[2])
    .map(isValid('first is bad'))

const second =
  IO(() => process.argv[3])
    .map(isValid('second is bad'))

const addArgs =
  liftA2(liftA2(add))

addArgs(first, second).run()

EDIT: Oh, also take note of the Array.of in that compose on the Err side. This is because String is a Monoid and Result accumulates Monoids in the Err for Applicative interaction. If both params where invalid, it would concat the two Strings. By throwing the value into an Array, it will accumulate the errors in separate entries.

@evilsoft
Copy link
Owner

evilsoft commented Oct 8, 2020

Oh a neat helper using the crocks curry (will only work with crocks) you can use is:

const lift2A2 = curry(
  compose(liftA2, liftA2)
)

@dalefrancis88
Copy link
Collaborator

if you use composeB in the above situation you get curry for free. Although I'm guessing this structure is more educational rather than do as i say :)

@JustinHoyt
Copy link
Author

@evilsoft Thanks so much for the revised example of IO! I made a dumbed down solution that I could understand how to write more easily:

const IO = require('crocks/IO');
const Result = require('crocks/Result');
const R = require('ramda');
const { Err, Ok } = Result;

const isNumber = (value) => parseInt(value) ? Ok(value) : Err('not a number');
const isPositive = (value) => value >= 0 ? Ok(value) : Err('not a positive integer');

const validate = R.compose(R.chain(isPositive), isNumber);

const first = IO(() => process.argv[2]).map(validate);
const second = IO(() => process.argv[3]).map(validate);

const addArgs = R.lift(R.lift(R.add));

console.log(addArgs(first, second).run().either(R.identity, R.identity));

After taking another look at your example after writing this I now think I understand a little more what your validateResult function is doing now. My validation will only log the first error it comes across rather than all the errors it finds, while yours will keep track of all the errors. I'm still hazy on const after reading the docs and how the compose is not making nested Arrays, but I'll do some more reading and try to unpack it some more.

@evilsoft
Copy link
Owner

evilsoft commented Oct 8, 2020

Nice!
As far as constant goes, that is the same things as ramda's always. So it is a binary function that will return the first value, and throw away any argument being passed to it. So that function will always pass (in this case) the selected error, into Array.of.

So, if you do not ant to accumulate Err and are going to use String as your error type, I would recommend using Either as opposed to Result. The only difference between the two is Either will not accumulate its left side when using Apply (ap), which is how lift works. In the provided example, if both are invalid you will get Err "not a numbernot a positive integer", because String is a Monoid

Also I may recommend using the predicates provided by crocks, we have one called isInteger that can check that. Also on that note, it looks like you may want an Integer, as it sits right now, Floats will make it through, because of the mix of validation and parsing (and the parsed value is not passed on). One thing I would recommend is to use parseFloat, so rounded floats are not in the mix and move the parsing out of the validation code, that way the concerns are separate and you get more reuse out of these tiny functions that just do one thing.

So with all those suggestions in mind, I put together another example, using Either and introducing things for you to learn (like composeK for composing things that use chain, bimap, concat, etc):

const IO = require('crocks/IO')
const Either = require('crocks/Either')

const add = require('ramda/src/add')
const bimap = require('crocks/pointfree/bimap')
const compose = require('crocks/helpers/compose')
const composeK = require('crocks/helpers/composeK')
const concat = require('crocks/pointfree/concat')
const constant = require('crocks/combinators/constant')
const curry = require('crocks/helpers/curry')
const identity = require('crocks/combinators/identity')
const ifElse = require('crocks/logic/ifElse')
const isInteger = require('crocks/predicates/isInteger')
const liftA2 = require('crocks/helpers/liftA2')

const { Left, Right } = Either

// Applicative A => lift2A2 :: (a -> b -> c) -> A (A a) -> A (A b) -> A (A c)
const lift2A2 = curry(
  compose(liftA2, liftA2)
)

// gt :: Number -> Number -> Boolean
const gt = curry(
  (a, b) => a < b
)

// validateEither :: (a -> Boolean) -> e -> Either e a
const validateEither = curry(
  (pred, err) => ifElse(pred, Right, compose(Left, constant(err)))
)

// checkInteger :: a -> Either String Integer
const checkInteger =
  validateEither(isInteger, 'not an Integer')

// checkPositive :: Number -> Either String Number
const checkPositive =
  validateEither(gt(0), 'not a positive Number')

// validate :: a -> Either String Integer
const validate =
  composeK(checkPositive, checkInteger)

// validator :: String -> a -> Either String Integer
const validator = tag => compose(
  bimap(concat(` (${tag})`), identity),
  validate,
  parseFloat
)

// first :: IO (Either String Integer)
const first =
  IO(() => process.argv[2])
    .map(validator('first'))

// second :: IO (Either String Integer)
const second =
  IO(() => process.argv[3])
    .map(validator('second'))

// Applicative A => A (A a) -> A (A b) -> A (A c)
const addArgs =
  lift2A2(add)

addArgs(first, second)
  .run()
  .either(identity, identity)

(note: purposefully written this way to help build an intuition around these ADTs and how they relate)

@evilsoft
Copy link
Owner

evilsoft commented Oct 8, 2020

how the compose is not making nested Arrays

Ohhhhh. I think I may know what is going on with your intuition on this lift madness, and why you had this line in your first example:

const result = mAdd(chain(first), map(second));

So when using lift, we are using the Apply/Applicative aspect of the data type. One of the significant ways this aspect differs from the Monad aspect, is that the type's effects are combined in parallel, over the lifted function. So BOTH effects are evaluated, and applied to the lifted function. The reason they are not nested, is because they are evaluated independently of each other and then combined with the function. This is why Result only accumulates Err values on ap and not chain.

Monads chain their effects in sequence.

@JustinHoyt
Copy link
Author

@evilsoft Seeing how you make a simple reusable function with validateEither blows my mind haha. I also didn't know about bimap so that's a super useful function to just learn! Thanks for the example, it's helping me identify opportunities to create generic reusable functions.

On a side note, I learned what I know about functional programming entirely from Professor Frisby's Mostly Adequate Guide to Functional Programming. It's a wonderful book, but I'm curious if you'd recommend any other readings or series to improve my functional programming in JS. I just found your ADT video series, so I'll try to work through that this weekend and next.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants