DEV Community

Ian Hofmann-Hicks
Ian Hofmann-Hicks

Posted on • Edited on

Monad Say What? (Part 1)

With all the buzz of late, Functional Programming this and Composition that, terms like Functor and Monad may have crossed your feeds and left you wondering, "what the heck are these people even talking about?". With such strange names and possibly even more foreign explanations that require a deep understanding of Algebra (the abstract kind) and Category Theory, it may be hard to see how these types of constructs fit in our comfy, day-to-day JavaScript code.

In this series we'll explore, from a JavaScript programmers point of view, what a Monad is and how they can be used to great effect in our everyday code. We will be focusing mostly on the usage of these types and not the theory behind them.

So for instance, instead of working to understand the following definition:

A Monad is a Monoid in the Category of Endofunctors

We will work toward understanding a more practical definition:

A Monad is a data type that allows for sequential application of its "effects" or "embellishments" while mapping its underlying data.

Now, while the second definition still may not be clear right now, I think we can agree that working to understanding those words and the meaning derived from how they all fit together seems a bit more approachable.

Understanding the first definition is critical when we venture out and create our own types. Although if you are anything like me, I like to get my hands dirty and build an understanding by first having a play with things and applying the theory when I have a good intuition of how use them. There are a slew of types already implemented in the wild that we can blissfully play with...without understanding the Maths behind them.

These posts assume an understanding of not only the JavaScript language, but how "currying", "partial application" and "function composition" is accomplished in Javascript. If you feel a bit fuzzy on these topics, there are many resources available on the webs to get you sorted out.

So without further ado, lets get cracking.

Part 1: The Algebraic Data Type (ADT)

Many times when people say "I used this Monad for this, or that Monad for that", what they really mean is: "I used this Algebraic Data Type (ADT) for this and that ADT for that". When looking at the code they are presenting, you find that they never touch the Monad part of the type, or in some cases the type is not even a Monad.

I would like to start things off by clearing up this point of contention with me. It seems like a minor thing, but I have found calling things a Monad when we really mean some ADT tends to lead to confusion when we are starting to build our initial intuitions around Monads and other aspects of a data type.

Before we can begin to understand what makes an Algebraic Data Type a Monad or not, we need to first get a feel around what an ADT is. The best way I can think of to broach the topic is provide a simple definition as to what an ADT is. Then demonstrate how an ADT in JavaScript is used in contrast to a (hopefully) more familiar imperative implementation.

Let's take a look at the data we will be processing with our examples:

// data :: [ * ]
const data = [
  { id: '9CYolEKK', learner: 'Molly' },
  null,
  { id: 'gbdCC8Ui', learner: 'Thomas' },
  undefined,
  { id: '1AceDkK_', learner: 'Lisa' },
  { id: 3, learner: 'Chad' },
  { gid: 11232, learner: 'Mitch' },
]

The data is a mixed Array that could contain values of any type. In this specific instance we have three types in play: POJOs (Plain ol' JavaScript Object) of varying shape, a Null instance and an Undefined instance.

Our examples will be defined with the following list of requirements:

  • Accept any value of any type at its input.
  • Unless the data is an Array with at least one valid record, an empty Object will be returned.
  • Return an Object of valid records keyed with a valid id from the included record, effectively filtering out any invalid records.
  • We define a valid record as an Object with a String keyed with id.
  • This function does not throw, no matter the input, and provides a reasonable default in the empty Object it returns.

From these requirements, we can implement an imperative function that does the following:

  1. Verify input is an Array, return an empty Object if it isn't.
  2. Declare a result accumulator for building our final result, defaulting it to an empty Object.
  3. Iterate over the provided Array and do the following for each item:
    1. Validate the item against our record criteria
    2. If passed, add the record to the result, keyed by the id value on the record. Otherwise do nothing.
  4. Return the result.

With a few helpers to help us with some type checking, we can provide an implementation like this:

// isArray :: a -> Boolean
const isArray =
  Array.isArray

// isString :: a -> Boolean
const isString = x =>
  typeof x === 'string'

// isObject :: a -> Boolean
const isObject = x =>
  !!x && Object.prototype.toString.call(x) === '[object Object]'

// indexById :: [ * ] -> Object
function indexById(records) {
  if (!isArray(records)) {
    return {}
  }

  let result = {}

  for (let i = 0; i < records.length; i++) {
    const rec = records[i]

    if (isObject(rec) && isString(rec.id)) {
      result[rec.id] = rec
    }
  }

  return result
}

indexById(null)
//=> {}

indexById([])
//=> {}

indexById([ 1, 2, 3 ])
//=> {}

indexById(data)
//=> {
//   9CYolEKK: { id: '9CYolEKK', learner: 'Molly' },
//   gbdCC8Ui: { id: 'gbdCC8Ui', learner: 'Thomas' },
//   1AceDkK_: { id: '1AceDkK_', learner: 'Lisa' }
// }

As we see, we have a strong implementation that meets our requirements and responds to any input we give it as expected.

As for our ADT implementation, we will be leaning heavily on the crocks library. Even though JavaScript is a fully functional programming language, it lacks some structures that appear in other languages that are not general purpose languages, but are strictly functional. As a result, libraries like crocks are typically used for working with ADTs.

Here is an implementation that implements the requirements using ADTs:

const {
  Assign, Maybe, composeK, converge, isArray,
  isObject, isString, liftA2, mreduceMap, objOf,
  prop, safe
} = require('crocks')

// wrapRecord :: Object -> Maybe Object
const wrapRecord = converge(
  liftA2(objOf),
  composeK(safe(isString), prop('id')),
  Maybe.of
)

// mapRecord :: a -> Object
const mapRecord = record =>
  safe(isObject, record)
    .chain(wrapRecord)
    .option({})

// indexById :: [ * ] -> Object
const indexById = records =>
  safe(isArray, records)
    .map(mreduceMap(Assign, mapRecord))
    .option({})

indexById(null)
//=> {}

indexById([])
//=> {}

indexById([ 1, 2, 3 ])
//=> {}

indexById(data)
//=> {
//   9CYolEKK: { id: '9CYolEKK', learner: 'Molly' },
//   gbdCC8Ui: { id: 'gbdCC8Ui', learner: 'Thomas' },
//   1AceDkK_: { id: '1AceDkK_', learner: 'Lisa' }
// }

One of the differences between the two implementation that I hope was noticed is the lack of familiar flow control and logic patterns in the ADT implementation. Things like for loops and if statements do not appear once in the second implementation. They are still there, of course they are still there, but when working with ADTs we encode these flows/logic in specific types.

For instance, notice that safe function that is used in a couple places? Take a look at the predicate functions passed to the first argument of those calls. Notice that the same checks are being done there, but instead of an if we are using the safe function that returns an ADT called Maybe.

Another thing you may have noticed is the lack of state anywhere in the second implementation. Every variable declared was a function, not a single JavaScript value in sight. We used two bits of state in the original implementation, result to put together our final result and a little helper called rec which just cleans up the code and keeps us from having to reference the indexed value from the Array.

We were able to get rid of the need of the for loop and the result variable, by using the function mreduceMap to fold each record over an Assign type. Assign lets us combine Objects similar to the way Object.assign does in vanilla JavaScript, removing the need to keep track of an accumulator like the result Object. So now that we have a means to accumulate, we can then remove the for loop by leaning on mreduceMap.

The Maybe, Assign, fold, etc. stuff does not need to be understood right now. I only mention them because I want to communicate that every pattern in the original implementation is present in the ADT version, there is no magic going on here. When we code with ADTs, we remove a lot of the mechanical bits like accumulation, logic, control flow and state juggling by encoding them in ADTs and let the types take care of all the "plumbing" for us.

The last thing I hoped was picked up on is how we are using what looks like a fluent api for chaining our operations together in the functions mapRecord and indexById. Seeing code like this may make us believe that we are working with traditional Objects and classes like a typical Object Oriented Programmer might. It is even reinforced when you hear these operations called methods (all the crocks documentation does this). These intuitions and misleading characterizations can get in the way of how we understand the way that ADTs are used in our day to day code.

Next time we will dig a little deeper on ADT usage by exploring how ADTs are not Objects in the sense that an Object Oriented Programmer would view an Object.

Exercises For Fun

  1. Take the first POJ (Plain ol' JavaScript) function and remove the for loop by using the reduce method available on Array.prototype. Take note on what happens to the result variable and how the default value of {} is applied.
  2. Take the first POJ function and, without using timers (setTimeout or setInterval), refactor it to be the MOST INEFFICIENT implementation you can think of. As you refactor, think about what you picked it as the MOST INEFFICIENT.
  3. Using either the first POJ function or your refactor from Exercise 1, identify the discrete actions/transformations that could live in their own functions. Then create those functions and refactor the main function to use them.

Additional Exercises (Also For Fun)

  1. We used a third-party library's type checking predicate functions for doing our type checks. Pick one of the predicates we used and implement your own version of it, throwing different values of different types at your implementation and see if it behaves as expected.
  2. If you happen to be versed in libraries like ramda or lodash-fp, implement the same behavior in a function using just the library you are familiar with. Compare the result of your function with the following pointfree version of the above ADT version:
// wrapRecord :: Object -> Maybe Object
const wrapRecord = converge(
  liftA2(objOf),
  composeK(safe(isString), prop('id')),
  Maybe.of
)

// mapRecord :: a -> Object
const mapRecord = compose(
  option({}),
  chain(wrapRecord),
  safe(isObject)
)

// indexById :: [ * ] -> Object
const indexById = records => compose(
  option({ error: true }),
  map(mreduceMap(Assign, mapRecord)),
  safe(isArray),
)

Top comments (13)

Collapse
 
antonrich profile image
Anton

Well, first of all, welcome. It's nice to see more functional folks here.

I see it like this.

You write, you gather feedback, learn from your mistakes and then you write a better article.

As a novice in FP I would like to see things like: refactoring someone's imperative code.

I started writing my comment without reading the article. So I thought instead of putting it off for later I'll read it now.

At this point I've read up to this:

These posts assume an understanding of "currying", "partial application" and "function composition". If you feel a bit fuzzy on these topics, there are many resources available on the webs to get you sorted out.

I understand those concepts exactly (maybe not in JS, but certainly in Elm and Haskell. I guess I can transfer my knowledge to JS).

Let's continue reading.

But I also have to warn you that I've already read about monad on the futurelearn course about Haskell. But I can't tell that I understand the monad or that I can use it.

// data :: [ * ]
const data = [
{ id: '9CYolEKK', learner: 'Molly' },
null,
{ id: 'gbdCC8Ui', learner: 'Thomas' },
undefined,
{ id: '1AceDkK_', learner: 'Lisa' },
{ id: 3, learner: 'Chad' },
{ gid: 11232, learner: 'Mitch' },
]

Why do you have null and undefined here?


Here's where I'm getting lost.

Our examples will be defined with the following list of requirements:

  1. Accept any value of any type at its input.
  2. When the data is not a non-empty Array of at least one acceptable record, an empty Object will be returned.
  3. Return an Object of valid records keyed with a valid id from the included record, effectively filtering out any invalid records.
  4. We define a valid record as an Object with a String keyed with id.
  5. This function does not throw, no matter the input, and provides a reasonable default in the empty Object it returns.

My questions are:

Why such requirements.


Continuing.

At this point I briefly looked at Crocks.

And now I started retyping:

{ gid: 11232, learner: 'Mitch' }

is gid purposeful or a typo?

And why you have some ids like numbers, a single number, or a mix of letters and numbers? Because data like this doesn't make sense to me. Is this how JS world looks like?


At this point I went to bed to lie down. I contemplated and when I returned I deced to look you up on twitter. Didn't find one.

Looked at your Github though.

You are the creator of Crocks!

Interesting.

At this point I'm watching your youtube video "pointfree and combinators".

Comb (ai) nators - is really a weard way to pronounce that word.


At this point I jumped around the article.

I have this to say:

I didn't think that functional JS would be that weird.

But I also understood that the purpose was to clean the data. I would actually start the article with that. Like we have some unclean data and we are going to clean that up.

You have actually demonstrated that but in the middle of the article. The question is? Would it be better to start the article with that? In my opinion it would.

I'm used to challenges on Codewars. It's such a good thing that they provide input and outputs. It's the first place where I look.

P.S. I rethought my earlier statement that functional JS is weird. It's actually not that weird.

Collapse
 
evilsoft profile image
Ian Hofmann-Hicks • Edited

Wow that is some amazing feedback. Thank you for taking the time for putting it all down. I cannot tell if your question are meant to be rhetorical, but I will err on the side of they are not and try to address them.

Why do you have null and undefined here?

In my experience with the web or working in dynamic languages, the data you receive may come from multiple sources and/or not ideally normalized for proper processing. I found data like I presented above in things like service oriented architectures and even when compiling scientific data from different instrument sources (even worse for double blind studies or peer review aggregation). So while fp concepts in JS is easily explained with simple curried add functions on Number, I wanted to provide a case a bit closer to real life.

Why such requirements.

Same answer as above.

is gid purposeful or a typo?

And why you have some ids like numbers, a single number, or a mix of letters and numbers? Because data like this doesn't make sense to me. Is this how JS world looks like?

A lot of this function is to normalize the input and separate the valid data points from the noise.

I contemplated and when I returned I deced to look you up on twitter. Didn't find one.

@evilsoft on twitter

Comb (ai) nators - is really a weard way to pronounce that word.

Nice, just one of the effects of reading about them in textbooks without a professor to say the word out loud I guess.

I didn't think that functional JS would be that weird.

It is not that Functional JS is weird, it is that working in a dynamic language presents some unique challenges that Haskell and Elm (and a few others) do not have to encounter.

But I also understood that the purpose was to clean the data. I would actually start the article with that. Like we have some unclean data and we are going to clean that up.

This is where I think I may not have been clear on the intent. This is supposed to start down our path of understanding how to use ADTs in our functional JavaScript. I wanted to start by contrasting how different typical imperative JavaScript looks compared to the same functionality implemented code with ADTs in JavaScript. It seems that you were fixated on the MacGuffin that was used to drive the "plot" and be the actual focal point itself (other then to provide the contrast). So this seems to be where the failure in communication on my part lies.

I take every bit of criticism, good or bad to :heart: and use to improve my material in my video, live code and egghead lessons. So I appreciate you taking the time and passion to make sure the the content I provide is getting the message across.

I am going to make some tweaks to this posting to address a couple of the things you pointed out. And will defiantly keep some of these points in mind while I edit the other 7 posts in this series.

Thank You Again For Taking The Time,
evil.

EDIT: One thing I forgot to mention, for things like do not throw and just return an empty Object and things like that. When working in dynamic languages we tend to use a concept of Reasonable Defaults. So instead of just erroring out if life is not what we expect to be, we can signal that all is not well by returning an empty Object (in this case anyway) as anything that relies on this program would get the Object it needs with the information that there is nothing to process.

Collapse
 
antonrich profile image
Anton

I cannot tell if your question are meant to be rhetorical, but I will err on the side of they are not and try to address them.

I was writing the comment while I was reading. Because I wanted to show you my thought process "in real time". If I were recording a screencast you would have heard me asking this question.

This way I can show where the friction occurs better.

Thread Thread
 
evilsoft profile image
Ian Hofmann-Hicks

Oh sorry about that.
Thanks so much for doing this, I find feedback like this very helpful!

Also did you happen to do any of the exercises? I am curious how they aided/hindered your understanding.

Thread Thread
 
antonrich profile image
Anton • Edited

To be honest, I haven't.

The reason is... actually, it doesn't matter what the reason is.

I will do what I'm supposed to do.

I will finish what I started.


Now I will write my comment again in the same style. I have attached my gedit window with the above all other windows option. I will reread the article especially the functional part (because some of the functions I don't understand). I will narrate as I go along. I will call this "narration style".


const { isArray, isObject, isString } = require('crocks')

I have to say that I'm not that proficient in JS. So correct me if I'm wrong. What I think I see here is destructuring. Pattern matching in other words. But on the right side there is a library so I don't really see how that works. I think I'll go right now and watch something on youtube to understand that part. I'll be as thorough as possible.


I opened youtube and searched es6 destructuring. I see a lot of videos. I'm going with the fun fun function video. If necessary I will narrate my process.

Here's an example:

let animal =
{ species: 'dog'
, weight: 23
, sound: 'woof'
}

// the destructuring
let { species, sound } = animal

// and from this I understand that the order doesn't matter.

Well, that's actually all I need at the moment. If order doesn't matter I can take out any function out of the library.

That's it. The video is 10 minutes long, I stopped at 1:27. Back to the tutorial.


At this moment I'm reading the requirements to really get them.

When the data is not a non-empty Array of at least one acceptable record, an empty Object will be returned.

This part, was a bit hard to understand. Because of the use of the "double negative". I understand that it's not. But you can simplify it.

I'd go with:

When the data is an Array with at least one acceptable record, an empty Object will be returned.

Tell me if I've "refactored" your requirement correctly.


Oh, I'm looking the type signature:

// data :: [ * ]

the "*" is not a type variable. But a wildcard. It kinda expects something unpredictable inside an array.

You know I also look at this:

From these requirements, we can implement an imperative function that does the following:

Verify input is an Array, return an empty Object if it isn't
Declare a result accumulator for building our final result, defaulting it to an empty Object.
Iterate over the provided Array and do the following for each item:
    Validate the item against our record criteria
    If passed, add the record to the result, keyed by the id value on the record. Otherwise do nothing.
Return the result.

Using a third-party library called crocks for some doing our type validation, we can provide an implementation like this:

And think that this could be written as comments in the code.

And that would be may be better, or not. Is there a way to find out?

At this point, I remembered that your purpose was to talk about ADT and monad. I went to the beginning of the article.

I'm not familiar with the word embelishment. I'm looking it up.

Embelishment is a decorative detail (elaboration, or addition).

I opened my editor to retype the example. I'm going back and forth between retyping and reading

"From these requirements, we can implement an imperative function that does the following:"

I do it slowly so I understand everything thoroughly. I'm going back and forth. I read one implementation, then retype and think. Then I read another one, and then retype again.

Oh, now I've noticed you don't have semicolons in the code. That's good.

I need to download the 'crocks' library. To run the examples. I`ve just installed crocks.
npm install crocks -S

It says "All you need to do is run the following to save it as a dependency in your current project folder".

How do I save it as a dependancy? What is -S by the way?


Okay, I've got the imperative code well, I think. Now, to the functional part. Now, I think I should the functional part in a separate file. Or not?
Gosh, it's 1:43 in the morning. I'm getting so sleepy. I will continue tomorrow when I wake up.


Alright, I have woken up.
I want to make sure that I get the function composition in JS.

In Haskell we do it like this.

func1 . func2 . func3 x x

So in JS that would be

composeK(func1(Int), composeK(func2(Int), func3(Int, Int)))

This is hypothetical, but I think I get it right.

What I see that I don't understand is Maybe.of. After that I looked at the converge function. It takes a lot of functions as arguments. Right now I'm looking at the documentation on converge. The reason I'm diving deep is because I want to do the exercises.

Now I see this. I doesn't take all the arguments, so to speak. So there is partial application at play. Maybe.of takes data as an argument and then it all reduces to one value.

At this point I haven't scroll down, but I remember you have given some explanations below. Now it's time to read them.

The function Safe led me to Maybe in the library. I'm scrolling and see chain there as well. I looked at both don't really understand them.

Let's continue anyway.

So, I see the reference to the fluent api. Which doesn't tell me anything, I tried understand it, but decided that is just too much.

Conclusion: the reference to the fluent api is completely unnecessary and will create more confusion.


Now I will go through the exercises.

  1. My immediate response is why would I use reduce when I just need to filter. I have problems with Haskell's fold function. So, I have the same problem with the reduce function.

If the reduce works with numbers and these numbers will be reduced to a singe value I understand.

But when the reduce (or fold in my practice) works with different data types other than numbers I have problems.

Okay, I just thought it about.

Here we have an empty array as an accumulator, and we will push object that are satisfy a predicate.
Now should I write this predicate as a helper function?

Here's my little refactor:

`js
let result = records.reduce(filter_records(records), [])

const filter_records = records => {
return isObject(records) && isString(records.id)
}
`
I think it's not correct.

Will this work?

`js

let result = records.reduce(filter_records(sum, records), [])

const filter_records = (sum, records) => {
return sum + (isObject(records) && isString(records.id))
}
`

I unfortunately, cannot find out at the moment. Because, I have no idea of how to connect Crocks. It is installed, I've never used package.json, I've not used any build tools yet.
Here, I need some help.

2 - 5. I feel that lack of knowledge of JS is a little hindrance and that every time I do something I need to dig deeper. I wouldn't say I'm lazy. But in this particular instance I am. Maybe I will return at some point and do the exercises. I liked the "reduce" exercise.


Why did I wrote all those comments?

  • Some people say that when you get the monand you lose the ability to explain it. I want to reflect and see if I get the monad and if it's really true.
  • So, I started and I need to finish. I want to be a finisher. I want to be a man of action and a man of my word. Being a man of my word is really hard for me. I do tend to commit to a lot of things but accomplish less. So it's kind of personal growth for me.
  • Your feedback is also valuable for me.
  • I have a slight interest in functional JS (But who knows how it's gonna work out in the future). But I hope that JS will be as functional as possible. That is my little contribution to that hope.
  • Another reason is. I may have acquired a bad behavioral pattern with the read later button. I once have read on Scott H Young`s blog that it's better to learn something deeply right away when you approach the problem rather than putting it off for later. This is my attempt to do exactly that.
Thread Thread
 
evilsoft profile image
Ian Hofmann-Hicks • Edited

So good.

So there are a couple things you've shown me I need to do. First of I need to update:

These posts assume an understanding of "currying", "partial application" and "function composition". If you feel a bit fuzzy on these topics, there are many resources available on the webs to get you sorted out.

To read more like:

These posts assume an understanding of not only the JavaScript language, but how "currying", "partial application" and "function composition" is accomplished in Javascript....

Also good call on the needing of crocks for the POJ example. I need to break those out into functions like:

// isArray :: a -> Boolean
const isArray =
  Array.isArray

// isString :: a -> Boolean
const isString = x =>
  typeof x === 'string'

// isObject :: a -> Boolean
const isObject = x =>
  !!x && Object.prototype.toString.call(x) === '[object Object]'

Yeah people should not have to install crocks at this stage in the game.

When the data is an Array with at least one acceptable record, an empty Object will be returned.

How about

Unless the data is an Array with at least one acceptable record, an empty Object will be returned.

Arrays in JS can be of mixed type, so we typically denote that with [ * ], as opposed to something like [ a ] where we expect everything to be that a. Well when our functions can work with mixed typed Arrays. In reality we want to strive for [ a ] or even better something like [ Number ], but sometimes at the edge, we need the [ * ]

You got mostly the compose bits down, except you would just use compose, composeK is for Kleisli arrows ( basically >=> or fish in Haskell). Also you can use either compose or composeK like this:

// x => fn1(fn2(fn3(x)))
const fn =
  compose(fn1, fn2, fn3)

My immediate response is why would I use reduce when I just need to filter.

So this is more than just a filter, we are change types. we move from an Array to an Object. So we want to reduce with {} as our empty on the fold, then only add valid records keyed by their valid id to the accumulator. Does that make sense?

Thread Thread
 
evilsoft profile image
Ian Hofmann-Hicks • Edited

Also just to help a fellow Haskeller out here is a quick JS -> Haskell key:

  • compose -> . (but compose is n-Ary)
  • composeK -> >=> (but composeK is n-Ary)
  • [type].of -> pure for Applicative Functor / return for Monad
  • [instance].chain -> bind
  • liftA2 -> liftA2
  • converge -> S' or Phoenix Combinator.
Thread Thread
 
antonrich profile image
Anton

I mostly don't know about them. So you are telling me new stuff that I should learn : ))) That's great. Thanks.

Collapse
 
antonrich profile image
Anton • Edited

Do you think the concept "having the end mind" is applicable here?

and

So, basically you first show this at the start of the article (introduce the end goal):

indexById(null)
//=> {}

indexById([])
//=> {}

indexById([ 1, 2, 3 ])
//=> {}

indexById(data)
//=> {
//   9CYolEKK: { id: '9CYolEKK', learner: 'Molly' },
//   gbdCC8Ui: { id: 'gbdCC8Ui', learner: 'Thomas' },
//   1AceDkK_: { id: '1AceDkK_', learner: 'Lisa' }
// }

And then you just show the imperative approach and then show how to refactor that approach into the functional one and what are the benefits.

Collapse
 
joelnet profile image
JavaScript Joel

Great article. Though I think someone unfamiliar with functional programming will get lost around the code block where you introduce: Assign, Maybe, composeK, converge, isArray,
isObject, isString, liftA2, mreduceMap, objOf,
prop, safe
.

There is a lot to digest in there. If you don't already have an understanding of every one of those functions, you risk losing the reader.

It could be helpful to have a step before this to explain these concepts. I think if someone already understands these, they will also already know what a Monad is too.

Cheers!

Collapse
 
evilsoft profile image
Ian Hofmann-Hicks

Thanks so much for the feedback.
The intent for that is not to understand it, but compare how things look differently. Personally I like to show the end goal, at a glance. It is my hope by the end of this series they will be able to understand it.

Maybe I could lead with the paragraph saying it is okay to not understand this, just look at the style. Thoughts?

Collapse
 
joelnet profile image
JavaScript Joel

I guess it depends on who your target audience is. There are a lot of functions a non FPer has never been exposed to.

Collapse
 
antonrich profile image
Anton

Reading your article taught me an important lesson:

I was reading your article in a linear fashion from to top to bottom. Instead of expecting the author to write in a style that I want. I can actually skim articles first and look for the inputs and outputs.

And then proceed to read the article in a linear fashion. This way I will have the bigger picture in mind and less friction when I start diving in.