Git Product home page Git Product logo

Comments (11)

evilsoft avatar evilsoft commented on May 28, 2024 3

@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.

from crocks.

JustinHoyt avatar JustinHoyt commented on May 28, 2024 2

@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.

from crocks.

JustinHoyt avatar JustinHoyt commented on May 28, 2024 1

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.

from crocks.

evilsoft avatar evilsoft commented on May 28, 2024 1

@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".

from crocks.

dalefrancis88 avatar dalefrancis88 commented on May 28, 2024

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

from crocks.

JustinHoyt avatar JustinHoyt commented on May 28, 2024

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.

from crocks.

evilsoft avatar evilsoft commented on May 28, 2024

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

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

from crocks.

dalefrancis88 avatar dalefrancis88 commented on May 28, 2024

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

from crocks.

JustinHoyt avatar JustinHoyt commented on May 28, 2024

@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.

from crocks.

evilsoft avatar evilsoft commented on May 28, 2024

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)

from crocks.

evilsoft avatar evilsoft commented on May 28, 2024

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.

from crocks.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.