getify / Functional-Light-JS

Pragmatic, balanced FP in JavaScript. @FLJSBook on twitter.
http://FLJSBook.com
Other
16.64k stars 1.96k forks source link

Tech Edit: Chapter 3 #48

Closed DrBoolean closed 7 years ago

DrBoolean commented 7 years ago

Loved this chapter. I feel it really gets the reader excited about this stuff!

What follows is a collection of mostly unnecessary ideas rather than "tech edits". I thought the tech was spot on and the examples were great so not much to do there. Either way, here are some thoughts:

1) You demonstrate equational reasoning several times both explicitly and implicitly.

I wonder if it's worth a mention that FP goes out of its way to preserve equational reasoning. Not sure if you'd want that, just saying something since these are great examples of it in action.

If it is a thing you'd want to consider, maybe also make it explicit each time, formally, instead of using english to explain refactorings:

p1.then( foo ).then( () => p2 ).then( bar ); // original
==
p1.then( foo ).then( constant( p2 ) ).then( bar ); // constant
== 
p1.then( foo ).then( ( v => () => v )( p2 ) ).then( bar ) // inline constant
==
p1.then( foo ).then( () => p2 ).then( bar ); // evaluate constant application to recover original

Anyways, just a thought. I understand this is supposed to be an informal book so no worries if this is too mathy - just something to consider so the reader gets a glimmer of the whole "reasoning about" thing and sees why purity can be so important later on.

2) That _ in p1.then( foo ).then( _=>p2 ).then( bar ); may confuse some

I get the "why the underscore argument?" question a lot. Maybe a note that _ is often used to mean "ignored parameter" in FP would be good or just move to () => p2.

3) "The advantage of currying here is being able to do something like curriedSum(1)(2)(3), which returns a function".

This line didn't quite drive home the distinction between partial and curry for me. I see the main difference/advantage is that currying is used at definition time and partialApplication is used "on the fly" by the caller. I suppose that's implied by the examples, but might be worthwhile to spell out since they are so similar.

4) "Don't just assume that uncurry(curry(f)) has the same behavior as f".

This is true for some libs, but lots do strive to make that isomorphism / adjunction (an iso between homSets) work, which then gives rise to the reader/writer monads (and their respective comonads). In any case, I'd mention it's false for this implementation only since it is true for some other libs.

5) Should it say something to the effect of "When currying, it's often useful to analyze argument position and put the "data" last". And "Usually one curries everywhere in this style". These techniques combined usually sidesteps the need for uncurry, partialRight, and reverseArgs".

I absolutely love how the printIf example ties in everything from the chapter. It is built up beautifully from the prior concepts. Although, having written code like this for a while, I found myself asking "why haven't I encountered the reverse/partialRight/uncurry pattern very often?"

I think it's two things:

So the common result in a ramda or haskell app would be something like: var printIf = flip( when )( output );

With the full code being:

const when = R.curry(function when(predicate, fn, args) {
  if (predicate( args )) {
    return fn( args );
  }
});

function output(msg) {
  console.log( msg );
}

function isShortEnough(str) {
  return str.length <= 5;
}

var isLongEnough = not( isShortEnough );

var printIf = R.flip( when )( output );

var msg1 = "Hello";
var msg2 = msg1 + " World";

var printShort = printIf( isShortEnough );
var printLong = printIf( isLongEnough );

printShort( msg1 ); // Hello
printShort( msg2 );

printLong( msg1 );
printLong( msg2 ); // Hello World

The tools here are worthwhile and valid, I just don't end up encountering the situation much in day-to-day code for the reasons stated above.

I'm not sure what to do with all of these thoughts and ideas, but I'm happy to have shared them. Either way if you decide to adopt them or not, I won't have emotions about it; just wanted to share my brain dump after reading.

Again, great chapter!

getify commented 7 years ago

Just curious, when you read this chapter 3, did you see the part about spreadArgs(..)? I added the "Spread 'Em Out" section earlier today and I wasn't sure if it was there when you read or not.

getify commented 7 years ago

I think your points about how printIf(..) would typically have been defined are spot on.

But I think they actually highlight something of the difference at the very heart of this book. That is to say, the desire to have a printIf(..) that takes two arguments (aka, not curried) -- thereby motivating the need for an uncurry(..) -- is because the style of code here, I call "FP Light", tries to look more balanced like normal (imperative) JS but infused with FP goodness.

The goal of "FP Light" is not strictly adherent/canonical FP, but rather a compromise where FP is used less formally to improve existing imperative JS while not paving over its overall essence in that rebalancing.

I wonder if perhaps I should actually add a short section here to make this exact point, showing both my version and yours for comparison, thereby pointing out to the reader not only the things you mention here (like currying with data-last, etc), but also reinforcing what our goal with this specific book and style of code are.

DrBoolean commented 7 years ago

I did see the Spread section - I refreshed later that day and thought I'd accidentally skipped it ha. There is one typo, btw: "imcompatible". Anyways, I dig it - just setting up a bunch of function utils for the further chapters and that totally belongs here.

For the printIf stuff... this is a good discussion to get me on the same page. It's interesting territory since the mentality is not just "port haskell wholesale" :)

My only (grain of salt) suggestion there, then, might be to change the signature to printIf(pred, msg) so one doesn't have to reverseArgs to make it compatible with when. It would read a little more englishy as well (i.e. printIf(isShortEnough, msg1)). Though I suppose that's the point of the chapter so I can't disagree with leaving things be.

I'll remind myself of the lite moving forward.

P.S. I'm going to use "englishy" from now on. What a word!

getify commented 7 years ago

:) good suggestions, I may change. thanks!

MajoDurco commented 7 years ago

Hey,

great chapter btw. I just want to ask about:

function partialRight( fn, ...presetArgs ) {
    return reverseArgs(
        partial( reverseArgs( fn ), ...presetArgs.reverse() )
    );
}

if it can be rewritten like:

function partialRight(fn, ...presetArgs) {
  return (...laterArgs) => fn(...laterArgs, ...presetArgs.reverse())
}

Thanks

getify commented 7 years ago

@MajoDurco

Almost, except for two things... for one, I was trying to illustrate a usage of reverseArgs(..), and for another, the final reverse() in your example isn't appropriate. Here's an example that shows the difference between the two. Should print "1 10 100 1000", but with your's, prints "1 10 1000 100".

function foo(a,b,c,d) {
   console.log( a, b, c, d );
}

var f = partialRight( foo, 100, 1000 );

f( 1, 10 );