Comments (11)
@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 theIO
, this will limit ALL interactions with the process arguments to theResult
lift
functions usually take instances of Applicatives as opposed to functions. You can use things likefanout/merge
orconverge
/compose2
for pushing the results of functions into thelift
.- As there are two effects here, we need to
lift
alift
so that both effects are applied. Kinda like doingmap(map(fn))
like we do forFunctor
composition, but instead of covarient Functors, these are Applicative Functors. - The
run
function is on the instance, as opposed to theTypeRep
, so it would have beenresult.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 Monoid
s in the Err
for Applicative
interaction. If both params where invalid, it would concat
the two String
s. By throwing the value into an Array
, it will accumulate the errors in separate entries.
from crocks.
@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.
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.
@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.
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.
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()
andIO.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.
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.
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.
@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.
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, Float
s 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.
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
.
Monad
s chain
their effects in sequence.
from crocks.
Related Issues (20)
- import crocks lib to crocks.dev for using in browser console HOT 2
- README.md example confusion with `curry` HOT 8
- `yarn run setup` problem with `node-sass` dependency
- `yarn run docs:dev` fails with `ReferenceError: primordials is not defined`. Node v12.
- Updating the framework used for docs HOT 11
- Adding support for Symbol properties HOT 1
- Incomplete docs HOT 5
- Documentation for React Application HOT 3
- Upgrading Tape to 5.x results in these tests failing
- `getProp` of `null` value returns a `Just` HOT 4
- [QUESTION]: ~> syntax, "lifting" functions HOT 2
- dimap in arrow HOT 2
- Allow ReaderT to work with Fluture HOT 4
- Add local function to Reader and ReaderT
- Add getPropOrError to crocks HOT 6
- Async 'race' and 'all' cancellation after first rejection
- Distribute src as ES Modules HOT 2
- Add pointfree Pair to array (pairToArray) function
- Add the StateT monad
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
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.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from crocks.