timoxley / functional-javascript-workshop

A functional javascript workshop. No libraries required (i.e. no underscore), just ES5.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Curriculum Review by @brianloveswords

brianloveswords opened this issue · comments

Hello World

Before starting...

  • "$input", hmmm confused about this
  • "program" or "module"? Should I be exporting a function or writing a
    program that looks at process.argv?
  • META: there should probably be more explanation as to why I'm
    doing this. I don't yet understand what this has to do with
    functional javascript. NOTE that it doesn't necessarily have to
    do with functional js, but some text saying "hey, this is just to
    get your feet wet, make sure you got the basics, etc" would be great!

During...

  • Okay, I'm trying process.argv[2]. I'm getting back a string, but
    that string is wrapped in quotes. That's weird, but maybe okay?
  • Verify doesn't like it. It looks like I'm getting the right
    string, but verify wants it without the quotes.
  • I guess I'll just .replace(/"/g, '') the string, even though I
    know that's not not correct, just so I can see what the solution is.

After finishing...

  • AH okay, so $input is a global that gets injected into the
    script. That is weird to me – I think it'd be better if either
    A) it used (and expected) process.argv[2] OR
    B) it expected a module with exactly one function that does what is expected.
    • But to counter my own point, this is functional javascript not
      functional node.js. So maybe the global injection is the right
      move? Not sure...

Higher Order Functions

Before starting...

  • Ah ha, so I see the global injection is going to be a common
    theme. I don't really like that – it feels to magical, I like the
    method used in levelmeup of building modules or taking things
    on argv. This isn't a dealbreaker by any means, but I think it
    does encourage better real-world practices.

After finishing...

  • Oh, I solved it totally differently:

     function repeat(fn, n) {
       if (n <= 0) return;
       fn()
       return repeat(fn, --n)
     }
    
     repeat($operation, 5)

    I do feel like this might be slightly less "clever" – and it
    teaches good recursion principles (always check your end
    condition first, always make sure your input converges on the end
    condition).

Basics: Map

Before starting...

  • The introduction could use a little more explanatory text as to
    what a map is and why it's useful – something like the following:

    "We often have to take a set of data and modify it before we
    output it. For example, say we get this back from our database:
    [{first: 'Brian', last: 'Brennan'}, {first: 'Alex', last:
    'Sexton'}]

    And we want a list of full names. We can do this with a for loop,
    but we have to manually keep track of indices and push to a new
    array on our own. A better way to handle this is by using a
    map. A map is a higher order function that operates each
    element of an array and returns a new array with each element
    transformed

    So to get our list of full names, our mapping function would be:
    function fullName(item) { return item.first + ' ' + item.last }

    And we'd call it like so: var fullNames = entries.map(fullName)

    Notice that the map function returns a new array – it doesn't
    modify the array in place. This is a very important principle in
    functional programming: it's always better to take input and
    return new output rather than modify things in place.

    Your exercise is to take an array of numbers and return a new
    array with each number replaced by itself * 2..."

After finishing...

  • I think this might be cleaner and help illustrate the power of
    passing function references around

     function double(n) { return n * 2 }
     console.log($input.map(double))

    I think the solution could also then explain the power of
    creating small, generically useful functions and then using them
    in maps to transform sets of data.

Basics: Filter

Before starting...

  • [workshopper] – is there a way to get code fences working in
    workshopper so that when the instructions are printed out the
    things fenced in ```js are syntax highlighted? (That'd be a rad
    module, and there's like a 90% chance substack has already
    written it).

After finishing

  • Again, I like seperating out the functions rather than using
    anonymous functions:

     function isShort(txt) {
       return txt.length < 50
     }
    
     function getMessage(obj) {
       return obj.message
     }
    
     function getShortMessages(arr) {
       return arr.map(getMessage).filter(isShort)
     }
    
     console.log(getShortMessages($input))

    I think it better illustrates composition.

Basics: Reduce

Before starting...

  • Some illumination for why a reduce is useful would be
    good. Perhaps even something that explains it in terms of mapping:

    "A reduce (also known as a "fold left") is useful when you want
    to take a bunch of values and turn it into just one. A good
    example of this is taking the sum of a series:

     [1,2,3,4,5].reduce(function sum(accum, value){
       return accum + value
     }, 0) // 0 + 1 + 2 + 3 + 4 + 5 = 15

    The first argument to the reduce functor is known as the
    accumulator – this keeps track of a value as the functor
    processes values. The second argument to .reduce is known as
    the seed value. This is useful for priming the accumulator so
    the functor doesn't have to do conditional checking against it to
    see whether or not it's the first value.

    It's important to choose the correct seed value for the operation
    you are performing! For example, 0 works well for addition, but
    if we wanted to get the factorial rather than the sum, we would
    want to use 1 instead:

     [1,2,3,4,5].reduce(function factorial(accum, value){
       return accum * value
     }, 1) // 1 * 1 * 2 * 3 * 4 * 5 = 120

    (Fun fact: this has to do with 0 being the identity value for the
    addition operation and 1 being the identity value for the
    multiplication operation [citation needed])"

  • Woah, the boilerplate is confusing to me! I think to properly
    explain reduce, the operation needs to be transparent – I think
    part of doing reduce properly involves selecting the proper
    seed. That's an important lesson to teach, and one that gets
    obscured if the operation is opaque. It's also a lesson about
    reduce but the boilerplate function says map.

    • AH I missed the part "i.e. Use Array#reduce to implement
      Array#map." – I think this is should be after a basic reduce
      lesson. This is more advanced reduce, I think.

After finishing...

  • I felt cheated by the "extra credit". None of the previous
    lessons taught the optional arguments, and the lesson instructions
    didn't say that we were supposed to implement
    Array.prototype.map exactly. The function signature in the
    "boilerplate" is also indicating that we should only implement
    something with 2-arity.
  • Again I think the solution is too clever – I'm not sure we should
    expect people to understand fn.call and fn.apply unless we
    make a lesson about it earlier on.

Partial Application without Bind

Before starting...

  • Should probably explain partial application a little bit
    first. Remind people about higher-order functions and tell them
    that in this case, we want to make a function that takes input and
    returns a new function.
  • If people have to do that argument variable "turn to a real array"
    hacking, that might be A) be a bad thing, or B) need to be
    explained.

After finishing

  • Yeah, doing the arguments trick without explanation isn't
    great. This should either be explained in an earlier lesson (along
    with call and apply), or there should be a solution that doesn't
    require it. I vote for the former. Here is my solution for
    reference, though I think the published one is probably better
    because Array.prototype.concat is rad and I don't use it nearly
    enough.

     function logger(namespace) {
       return function logFn() {
         var args = [].slice.call(arguments)
         args.unshift(namespace)
         return console.log.apply(console, args)
       }
     }
    
     $test(logger)

Partial Application with Bind

Before starting...

  • Function arity should be explained (in an earlier lesson)

After finishing

  • Pretty good!

Function spies

Before starting...

  • I think the purpose of spies – testing, debugging – should be
    explained upfront. Also that spies should probably only be used
    for testing, debugging because the behavior is opaque to anyone
    down the line. (I am very open to debate on this issue!)

After finishing

  • Ah, I was confused by the naming – Spy with an uppercase S – and
    I was expecting to build a constructor. I guess in a way it is?
    I'm torn on this one. My solution was this:

     function Spy(object, methodName) {
       if (!(this instanceof Spy))
           return new Spy(object, methodName)
    
       this.count = 0
       var originalFn = object[methodName]
    
       object[methodName] = function secretAgent() {
         this.count++
         return originalFn.apply(object, arguments)
       }.bind(this)
     }
    
     $test(Spy)

Conclusion

This is awesome. I think with some interstitial lessons about call
and apply, maybe one about reduce before implementing map with
reduce, this can be a super rad workshopper! I'm definitely willing
to help with creating/editing the intro text to add more
theory/reasons behind why someone would want to use FP techniques in
JS – I think that's important. Thank you so much for getting this
started!

ALSO, forgot to make it clear – I'm totally willing to make some PRs for this stuff. I wanted to review first to get your opinion before I jumped in and started making changes! Let me know which things you think are worth adding/modifying and I'll get on it.

Wow thanks for this feedback, exactly what I was looking for!

Some of the points you've raised are listed in here: #2

magical $injection

Injection is horribly magical, but is just as magical as process.argv[n] for someone who doesn't use node, and the tutorial is aimed at non-noders. I'm thinking the export a function and passing in the arguments would work pretty well and wouldn't require too much noise, and reduces the magic. I'll refactor.

Meta Info

I relied a bit on being able to physically talk people through each lesson. Definitely needs more explanation about what the purpose of each lesson is and some more detail about how to work the functions. Understanding how reduce worked at all was particularly difficult for many.

Repeat Solution

Agreed, the recursive solution is better. Perhaps an introduction to recursion should occur prior, or perhaps this could be 'higher order functions & recursion' with some more introductory text.

Map

The point here was to give a minimal example to help people connect the for-loops + accumulator array pattern with maps… As in, "any time you see this pattern, it's a good case for using map". People are very attached to their for-loops, even after I told them not to use them, people kept writing them! I'm thinking I need to use esprima to parse their code and fail on any for-loops. I'm happy to change the exercise to operating over an array of objects, as that's probably more real-worldy.

Filter

I have a feeling it wouldn't be hard to implement fences, even if it just changed the colour and indented it.

Reduce

Yeah this one was a bit tricky on multiple fronts. Everyone stumbled on the "extra credit" and I've removed it and added a simpler reduce example (available on master branch). And I'll introduce apply/call/arity on their own in a prior exercise.

Partial Application without Bind

Yep, the arguments -> real array thing needs explaining. Sounds like it could fit well into the apply/call/arity exercise.

Spy

I find myself using this pattern for monkey patching bad behaviour of other libraries. Good practice? probably not. But it's easier/safer than running/depending on my own fork.

Capitalisation is a pattern I'm seeing recently (e.g. MuxDemux examples) where the capitalisation simply means, 'this is a factory', rather than 'this is a constructor', which I guess is a specific type of factory.

Conclusion

Thanks a lot for your detailed feedback!
Please go ahead and add your meta explanations in, they all read well, I like. I think refactoring the $magic into a simple module.exports = function() { } is a pretty good idea, I'll most likely change that.

I was going to send this as an email until I found this discussion:

Firstly can I to take the opportunity say how much I have enjoyed working through (most) of your node school module. If I have appeared critical on the forum I apologise. Some of the exercises I have found frustrating but mostly at my own inability to work out the solution. I know you have extended an extraordinary amount of time and effort in putting this module together and for that I offer a big thank-you.

I have learnt a lot of tips and techniques so far and, looking at your solutions, which are always neat, concise and very elegant, your knowledge of JavaScript is clearly at a level that John Resig terms Ninja Master. I am certainly trying to emulate your style in my own coding. I also suspect, from the way the exercises have jumped forward, that you're one of those people that can sketch out the solution to a complex maths problem in 3 lines whereas it would take someone like me about 10.

That said, I would like to offer a few suggestions for the module.

Higher Order Functions

Having worked through all of the learnyounode module and half of stream-adventure, one of the things that is very noticeable is the way that the student's answer receives it's input fromworkshopper. In the other modules it is always a single item; url query string, file path, some kind of stream, etc. In some of your exercises though, the input is doubled.

The second exercise, Higher Order Functions, introduces the student to the concept of a function receiving a function as an input, but then we don't require a higher order function solution again until Basic: Every Some. At this point we require a function that returns a function. This latter exercise not only introduces a new type of functional programming but also provides the student with a basic framework that will be required for future exercises. So, in that light...

a) Basic: Every Some

I wonder whether the introduction to this module might not be changed to something like:

In lesson 2: Higher order functions, we implemented a function that receives another function as it's input. Here you will be required to implement a function that returns a second function that will also receive input. This framework of higher order functions to receive 2 sets of input will be an important coding paradigm for completing future lessons.

b) Partial Application without Bind / Partial Application with Bind

Then, when we get to the later lessons of Partial Application without Bind and Partial Application with Bind the current instructions of "Your implementation should take a namespace string, and return a function that prints
messages to the console with the namespace prepended." might refer the student back to Basic: Every Some.

In the lesson Basic: Every Some you were introduced to the concept of a higher order function that returned a second function to be invoked with further arguments. Here your implementation should be a function that accepts a namespace string and returns a function that takes a message which should then be printed to the console with the namespace prepended

Also, I found the implementation a little confusing in that I was expecting first: a namespace, then second: a series of sentence strings. Instead it appears that the implementation is a single namespace then a series of words that need to be concatenated to form the sentence.

under Arguments I feel this could described as:

"The arguments to the returned function are a series of words which will need to be combined to form the message body."

A similar description for the implementation and second input could be applied to Partial Application with Bind.

Basic: Reduce

I personally found the number of new concepts introduced in this exercise to be a bit overwhelming. The previous lesson, Basic: Every Some introduced the student to a number of Array methods and encouraged them to investigate the return from each function in order introduce the concept of chaining methods together. The problem with Basic: Reduce, as I mentioned in my forum posting, is your very clever and unique use of the Array#reduce method as a form of iterator. In addition, the exercise also introduces a new concept of the data cache.

I wonder whether an intermediate exercise might not assist the student in gaining insight into iteration and caching. Say, Basic: Iterator with Cache. Essentially do the Basic: Reduce exercise using any form of loop the student chooses but introducing loops as a way of iterating over an array and storing the results of the loop function in a data cache.

Then, once the student understands the concepts of caching and iteration, introduce Basic: Reduce with

"Task: Given an Array of strings, use the concepts of data caching and array iteration from the previous exercise to implement Array#reduce to create an object that contains the number of times each string occurred in the array. Return the object directly (no need to console.log)."

The student may not be familiar with howArray#reduce actually works. I certainly found it difficult to conceive how to get such a reduction algorithm to store data. So, under hints it might say.

"Consider using a data cache as the optional second parameter to the reduce() callback. Consider that the reduce() method works by taking the first 2 items of the array and replaces them with the single item returned from the callback"`

Partial with Bind

I personally struggled to get my head around apply(), call() and, in particular bind(). As with Basic: Reduce I felt the leap from Partial without Bind to Partial with Bind too great, especially considering the solution you were after. Not least that it requires the student to be aware of the concept of Console as a global object. Again, might I suggest an intermediate step which, in a way, was how I completed this exercise in such a verbose fashion. If one considers Object#bind as a kind of recipe then we need 3 ingredients: a function we wish to bind, and object to bind it to and some additional arguments to spice things up as required. Perhaps you could return to the Basic: Call exercise and duck counting with the property 'quack' and structure an intermediate exercise:

"Implement a function that accepts a number of arguments; an animal, it's associated sound and possibly additional qualities. Create an animal object and a separate function literal that outputs the name of each animal with it's associated sound. Use Object#bind in the form function.bind(object, arguments).
Hint: binding a function to an object returns a new function."

The other difficulty I found in analysing your solution was the use of the Console object. Returning "console.log.bind(console, namespace)" appears as if you're using a method from the same object that you're binding the method to. If the student is still at the function.bind(obj, args) stage, like me, then this is rocket-science stuff. Also, many students will be unaware that Console is actually a global object with it's own methods. The Console object also seems unique in that if you use console.log.apply(console, arguments) it prints out the arguments but not if you just use console.log(arguments) which gives them as an object with numbers as keys. Like your solution to Partial with Bind, this construct looks as if it's applying an object's own method to itself.

Partial without Bind

Following on from above. Your solution to this exercise makes use of the console.log.apply(console, ..) concept. Neither of the two links which are given as references explain how you would use the Console object in this way. On a personal note, I found I was looking for a way to use Object#apply to manipulate the incoming data not as a way to structure my output. Again, without wishing to make this module too big, perhaps there's room for an exercise around the Console object.
For example, Tom Ashworth, one of the mentors at the node school meet-up references this construct in his article "useful js call and apply" http://phuu.net/2012/11/16/useful-js-currying-and-bind.html

"Let’s say you’re passing a callback to someone else’s library and you’re expecting data back, but you don’t know how many arguments you’re going to get. There’s a simple trick to allow you to console.log everything that’s passed to your callback.

var args = [].slice.call(arguments, 0);
console.log.apply(console, args);

"Because apply takes an array of arguments to be passed to the function, you can sling it the array of arguments you received and have them logged out, all nicely formatted."

The use of Array.prototype.slice is introduced in Basic: Call although the narrative for that exercise is already quite long enough without introducing the Console object. Dare I suggest yet another exercise around the Console object? Sadly, I can't think of any useful implementation off hand.

I hope this has been of some use. I still have 7 more exercises to complete and will report back at the end.

Hi @timoxley - thanks for an awesome workshop.
Have you had another curriculum review since this one? Sounds like you've probably taken on a lot of the original comments and fixed a bunch of things...
I've got about halfway through the workshop, and written a few notes on things I think could be improved, so I could easily share them in the same way (in this or another issue).

In the meantime, just wanted to put in my responses to the comments above.

Hello World

loving the way you've now set up each exercise to export a function. Much prefer this to the process.argv[n] option, as I think it is closer to what I'm dealing with on a daily basis.

I'm a slightly more experienced coder, so I found the first few exercises fairly easy, the middle ones harder, and haven't finished the last ones yet.

Map

agree that the solution could also look like this:

function double(n) { return n * 2 }
 console.log($input.map(double))

could you show both options in the solution, so people can see that they are equivalent, and just use different styles?

Tim - you mention "any time you see this pattern, it's a good case for using map" here in this issue - put that into the exercise itself too - either in the problem or solution text.

Filter

Again, I like seperating out the functions rather than using
anonymous functions:

I like how you've named your function in place inside Array.map call - we always try to do this in order to make the error messages and stack trace more meaningful. Maybe you can add a little part explaining the benefits of this?
+1 for illustrating reduce with summing and with doing a factorial, to explain the seed value.

Partial application without bind

I also really struggled to concatenate a string onto the front of an array properly - took me ages and loads of trial and error. I also think it might be nice to put this stuff into a separate exercise.

Conclusion

Tim you've asked @brianloveswords to add in the meta descriptions, but he hasn't put them in yet has he? Shall we go ahead and do this?

Lastly

@timoxley what are your thoughts about putting in any of the changes that @magwitch has suggested? I agree that the pace of learning is fast, often with multiple concepts to be understood between exercises. Would just be good to hear your response to their comments.
I also would like encouragement to type console.log(console) into the node REPL - it took me a long time before I did this, but it was a real eye opener to understand that it was just a global object!

Ps.

I seem to have ground to a halt around exercises "Implement Map with Reduce" and "function spies", and I can't see any other users comments or issues around the later exercises. Are a lot of people dropping out of the workshop before they reach the end, do you know? Is there any way we can track the point at which people drop out, or stop making progress?

Pps.

How do you feel about using ascii art for when you complete a workshop? https://github.com/eiriksm/workshopper-hooray