By Developers, For Developers

Historical errata for Web Development with ReasonML

|j};
};

let makeErrorRow: string => string =
input => {j|

\
|j};

let createRow: t => string =
orderResult =>
switch (orderResult) {
| Belt.Result.Ok(order) => makeOrderRow(order)
| Belt.Result.Error(input) => makeErrorRow(input)
};

let tableTop = {j|

PDF PgPaper PgTypeDescriptionFixed onComments
40TYPO

‘We’d like to use Belt.Option.map() to eliminate the switch in line 12’
I believe this should say line 11, since line 12 from the code example (seen on page 38) is already ‘inside’ the mentioned switch:
’ | Some(value) => …’

2018-11-26Thanks. I have changed the wording to mention the switch but focus on toFixedWithPrecision, which is the important part.
47SUGGEST

Maybe instead of using parcel to bundle files and then pointing browser to some file system path it would be better to suggest using parcel in ‘development mode’ which automatically starts a web server (by default on localhost:1234) with hot replacement enabled?
It’s then as simple as running ‘parcel src/index.html’ and going to localhost:1234 in a new browser tab.

Also to avoid installing parcel in a project (or globally) one could use npx (npx parcel src/index.html).

2018-11-26The server with localhost:1234 is mentioned in a sidebar on page 48.
63TYPO

In the PDF version link in the ‘Using map() , keep() , and reduce() with Lists’ section seems to be missing space between ‘of option’.

2018-11-26This seems to be a problem with the way the typesetting engine does spacing; there is definitely a space in the source file. I will ask about this.
75SUGGEST

Last case in switch inside ‘toOrder’ function seems a bit confusing:
| None => Error(s)

IDE with ReasonML plugin shows that Error is Belt.Result.Error, so maybe it’s worth to make it consistent in whole function (having Belt.Result.Ok/Error everywhere or just Ok/Error) or add a sentence saying that type inference knows that Error = Belt.Result.Error.

2018-11-26Fixed, thanks.
76TYPO

‘The result of Js_string.match()’ should be ‘Js.String.match()’

2018-11-26Although Js_string.match does work, that’s an older version of the library. The preferred way to write it is, indeed, Js.String.match() -- will fix that, thanks.
1SUGGEST

This is a general suggestion. Just about all the non-Javascript technologies ( reasonml, websharper, fable, elm,…) make too much emphasis in convincing Javascript programmers to embrace functional programming. Too much. But they ALL make no effort in helping non-Javascript programmers embrace js technologies.

The one advantage I’ve seen of ReasonML over other functional web languages is it’s good toolchain. It is the easiest approach to web programming I’ve found for non web functional programmers. Make a bit less emphasis on convincing imperative chaps and more on helping non web programmers use ReasonML.

2019-08-07Thanks. That will take some thought; let me discuss it with the editor. I don't think I will be able to put in anything in this area at this moment.
46ERROR

let calcButton = Doc.getElementById(“calculate”, Dom.document);

should say

let calcButton = Doc.getElementById(“calculate”, D.document);

2018-11-26Thanks. Fixed.
54SUGGEST

Chapter 4 is quite confusing.
1. First, should we create the project using the same template as the previous exercises?
2. We mentioned parcel, it should not be installed as a global dep since that’s a bad practice, what if you recommend it to be added as a devDep and create a npm script for it?
3. I feel that there’s no real structure of this chapter for someone new to frontend development - This could be a BETA release issue though!

Looking forward for more updates!

2018-11-271) Added a paragraph that says we start the project as we have done so far, with: \nbsb -init shirts -theme basic-reason \n2) and 3) - giving these some thought.
3TYPO

4th bullet point. “ReasonML is requires…”. Remove the word “is”.

2018-11-26Thanks; will be fixed in next beta.
7SUGGEST

“The naming convention for ReasonML follows the JavaScript convention
where each word is capitalized.” Would add the word “camelcase” between Javascript and convention.

2018-11-26Good idea!
119TYPO

Last sentence in ‘Handling Missing Fields’ hint/sidebar:
‘You can test
for this situation by using the Js.nullable.isNullable() function’

Should probably be ‘Js.Nullable.isNullable()’

2018-11-27Thanks; will fix.
46TYPO

Webapi.Dom.EventTarget.addEventListener should be D.EventTarget.addEventListener for consistency.

2018-11-26Will fix; thanks.
26TYPO

Output has three significant digits (DE: € 65.100) while code only specifies two.

Also for p.25 consider a variation to drive home punning further, i.e.

let principal = 10000.0;
let apr = 5.0;
let digits = 2;

Js.log(“Loan of 10000 at 5%”);
Js.log2(
{js|US: $|js},
toFixed(usPayment(~principal, ~apr), ~digits)
);
Js.log2(
{js|UK: £|js},
toFixed(ukPayment(~principal, ~apr), ~digits)
);
Js.log2(
{js|DE: €|js},
toFixed(dePayment(~principal, ~apr), ~digits)
);

2018-11-26Fixing the incorrect number of decimals.
24SUGGEST

An interesting variation may be:

let callGermany = call(“049”);
let callBerlin = callGermany(“030”);

2018-12-05I might add that in the code as a comment.
21SUGGEST

After functions/src/Annotations it could be useful to explore that the types that appear in the interface file can actually be used in the implementation file. So for example rather than using:

let avg = (a: float, b: float) : float => (a +. b) /. 2.0;

one could use:

let avg: (float, float) => float
= (a, b) => (a +. b) /. 2.0;

Now granted people with a primary background in some C-style language(s) will tend to prefer the first (inline) version - simply because a sense of familiarity gives them the warm fuzzies.

But the second version does not conflate the notion of the function’s TYPE with the parameter names that are being used to support the IMPLEMENTATION (separation of concerns). And aside from the fact that it’s the type being used in the interface, it is also the type that is reported in rtop when one defines the function.

Furthermore it can become clear that the labels ARE considered part of the type, i.e.:

let payment: (~principal: float, ~apr: float, ~years: int=?, unit) => float
= (~principal, ~apr, ~years=30, ()) => {

}

(verbosity notwithstanding)

Interestingly enough at this time refmt doesn’t seem to have an opinion on the style of annotations to prefer as it simply leaves them alone.

2018-11-26Interesting -- I did not know this. I will ask about it in reasonml.chat and see what the consensus is of why the separated type is not preferred. \n \n[Later] I have put in an example and some verbiage of how your preference may depend on whether you are coming to ReasonML from Haskell or TypeScript.
21TYPO

code/functions$ bsc -bs-re-out lib/bs/src/Annotations-SimpleFunctions.cmi > src/Annotations.rei

didn’t work for me - though that may be a platform issue (macOS). I needed to use:

code/functions$ bsc -bs-re-out lib/bs/src/Annotations-Functions.cmi > src/Annotations.rei

giving me the impression that the cmi file name suffix is based on the project directory name (which is just “functions”). Similarly on page 22 I’m seeing

[1/1] Building src/Currying-Functions.cmj

not

[1/1] Building src/Currying-Simplefunctions.cmj

2018-12-05This was a problem with the bsconfig.json file; I had changed the name of the directory from simple_functions to functions without changing the bsconfig.json file, so it got the wrong name. I've fixed the config file, and will fix the text to match.
7SUGGEST

Nitpick: If you do plan on using the term “camel case” please be precise and either use “pascal case” or “lower camel case”. People seem to constantly overlook that “camelCase” (i.e. lower camel case) doesn’t sound any different than “CamelCase” (i.e. upper camel case) in verbal communication, i.e. using the unqualified term is inherently ambiguous.

It’s largely in a Microsoft context where the “Pascal case” vs “Camel case” convention has been adopted. (online search: “Microsoft Capitalization Conventions”)

2018-11-26I'll add "lower camel case".
33TYPO

datatypes/src/ParamShirtSizes.re

Js.log(stringOfShirtSize(veryBigSize)); /* output: M */

should be

Js.log(stringOfShirtSize(veryBigSize)); /* output: XXXL */

2018-11-27Will fix; thanks.
33SUGGEST

It may be worthwhile to point out that while pattern matching does destructure, destructuring doesn’t imply pattern matching (some people seem to get the two confused).

ECMAScript has destructuring assignment but ECMAScript Pattern Matching is only a Stage 1 TC39 proposal.

To quote Rich Hickey “The big difference is simple: Pattern matching is a conditional construct and destructuring isn’t.”.

2019-08-07Leaving as is.
41SUGGEST

You have set up the perfect opportunity to introduce (fast) pipes - especially before the “It’s Your Turn” exercise.

let makeDisplayText: option(string) => string =
output =>
switch (output) {
| Some(value) => “The result is ” value
| None => “Could not calculate result.”
};

let method2: string => unit =
input =>
input
->toFloat
->Belt.Option.flatMap(reciprocal)
->Belt.Option.map(cube)
->Belt.Option.map(Js.Float.toFixedWithPrecision(~digits=3))
->makeDisplayText
->Js.log;

method2(“2.0”);

and possibly reference “Railway Oriented Programming” (ROP).

Plus - it may be and idea to run all the code samples through refmt first.

let myMap: (option(’a), ’a => ’b) => option(’b) =
(optValue, f) =>
switch (optValue) {
| Some(value) => Some(f(value))
| None => None
};

let myFlatMap: (option(’a), ’a => option(’b)) => option(’b) =
(optValue, f) =>
switch (optValue) {
| Some(value) => f(value)
| None => None
};

2018-11-26I really like this idea. It may take a fair amount of rearranging of existing text. I have to give this one a lot of thought. \nBy the way, as I understand it, current “best practice” is to explicitly make the first function call in the chain. Instead of: \n \ninput \n ->toFloat \n ->Belt.Option.flatMap(reciprocal) \n \nit should be: \n \ntoFloat(input) \n -> Belt.Option.flatMap(reciprocal)
40SUGGEST

“Let’s use currying to call Js.Float.toFixedWithPrecision() with just one of the arguments—the desired number of decimal points.”

Is “currying” accurate here or a mere colloquialism? Typically what is often referred to as “currying” moves through the arguments left-to-rigtht:

let test: (string, string) => string = (x, y) => x y;

let test1 = test(“A”);

Js.log(test1(“B”)); /* output: ‘AB’ */

The documentation types Js.Float.toFixedWithPrecision as:

val toFixedWithPrecision : float -> digits:int -> string

So Js.Float.toFixedWithPrecision(~digits=3) is binding the right-most argument - so I think “partial application” would be more accurate.

Furthermore according to the Haskell wiki:

“Currying is the process of transforming a function that takes multiple arguments in a tuple as its argument, into a function that takes just a single argument and returns another function which accepts further arguments, one by one, that the original function would receive in the rest of that tuple.”

“Partial application in Haskell involves passing less than the full number of arguments to a function that takes multiple arguments.”

Seems to me “currying” is often (possibly incorrectly) used describe the process of partially applying the left most argument of a curried function.

2018-11-26I think I have a way to work this in...
40TYPO

“Instead, we use Belt.Option.map(), which takes a non-option value and a function with ordinary input and output values (like cube()) as its parameters.”

should be

“Instead, we use Belt.Option.map(), which takes an option value and a function with non-option input and output values (like cube()) as its parameters.”

2018-11-26Good catch - will fix.
39TYPO

“But all those switches get make the code harder to read.”

should be

“But all those switches make the code harder to read.”

2018-11-27Will fix; thanks.
37SUGGEST

For datatypes/src/OptionShirtSizes.re please consider the following variation:

let toFixed = Js.Float.toFixedWithPrecision;

let makeDisplayText: (string, float) => string =
(input, cost) => {
let costStr = toFixed(cost, ~digits=2);
{j|Your $input shirt costs \\$$costStr.|j};
};

let displayPrice: string => unit =
input => {
let size = shirtSizeOfString(input);
let amount = price(size);
/* i.e. bind switch result to an identifier … */
let text =
switch (amount) {
| Some(cost) => makeDisplayText(input, cost)
| None => {j|Cost determine price for $input|j}
};
/* … and then log the value bound to the identifier */
Js.log(text);
};

displayPrice(“XXL”);

Using the Js.log inside the switch expression is symptomatic of a “statement mindset” - I think the “expression mindset” needs to be promoted here.

Similarly for the final switch statements in datatypes/src/BeltExamples.re on p.38 and p.40.

2018-11-26That sounds good.
35SUGGEST

When it comes to coding exercises I find the “requirements gap” in a description like

“Write a function that converts a colorSpec to a string”

at best an “annoying distraction”.

Lack of information may be a good challenge for improving more general problem solving skills but it distracts from the core “coding exercise”. As a reader I find specifics like
/* Output: */
Js.log2(“White:”, stringOfColorSpec(White)); /* White: rgb(255,255,255) */
Js.log2(“Black:”, stringOfColorSpec(Black)); /* Black: rgb(0,0,0) */
Js.log2(“Gray(0.5):”, stringOfColorSpec(Gray(0.5))); /* Gray(0.5): rgb(127,127,127) */
Js.log2(“RGB (255,255,255):”, stringOfColorSpec(RGB (255, 255, 255))); /* RGB (255,255,255): rgb(255,255,255) */

much more satisfying and to the point.

2018-11-26I prefer open-ended exercises, but having a specification of the intended output is probably good here.
52SUGGEST

let _ =
Doc.getElementById(“price”, D.document)
-> Belt.Option.map(Elem.setInnerText(_, priceString));

Don’t gloss over this code because there is something new going here that could be easily missed by the “casual observer”.

What doesn’t help is that you suddenly abandoned your explicit piping style. To stay consistent it should have read like this:

let _ =
Doc.getElementById(“price”, D.document)
-> Belt.Option.map(, Elem.setInnerText(, priceString));

There are TWO underscores with very different meanings. The first simply directs the option value into the proper argument position. The second underscore skips the first positional argument of the “Elem.setInnerText” function in order to partially apply the “priceString” argument.

let test = (x, y) => x y;
let test1 = test(_, “B”);
Js.log(test1(“A”)); /* output AB */

In fact refmt doesn’t modify the second version while it rewrites the first as:

let _ =
Doc.getElementById(“price”, D.document)
->Belt.Option.map(_x => Elem.setInnerText(_x, priceString));

1) not explicitly marking the piped value destination (because it wasn’t marked in the first place)
2) making the partial application quite explicit

2018-11-26Will add further explanation.
51SUGGEST

“Similarly, unitPrice is bound to an option(float).”

It occurs to me at this point that it may not be a good idea to be using floats to represent monetary values - it may give some people the wrong idea. Given ReasonML’s strictness it’s not as simple to accidentally turn an “integer” value into a “float” (I know they’re all IEEE 754 under the hood - though there is a Stage 0 TC39 proposal for introducing decimals) as it is in JavaScript.

Given the safe range of integer values

> console.info(Number.MIN_SAFE_INTEGER);
–9007199254740991
> console.info(Number.MAX_SAFE_INTEGER);
9007199254740991

it may make sense to handle the monetary amounts entirely as integers and only introduce float for final discounts and display in dollars.

2018-11-26I understand your concern, but I think I am going to leave this one as it is rather than introduce another concept in addition to the one I’m concentrating on in this section.
51SUGGEST

… continued, example:

/* Convert fractional monetary unit (cents) to basic monetary unit (dollars) */
let basicOfFractional: int => float =
fractional => float_of_int(fractional) /. 100.0;

let priceString: string =
switch (maybeTotalPrice) {
| Some(total) =>
totalbasicOfFractional>Js.Float.toFixedWithPrecision(~digits=2)
| None => “”
};

2018-11-26
47SUGGEST

This simple setup seems to work reasonably well:

package.json:

{
“name”: “shirts”,
“version”: “0.1.0”,
“scripts”: {
“clean:bsb”: “bsb -clean-world”,
“build”: “bsb -make-world”,
“watch:bsb”: “bsb -w”,
“watch:parcel”: “parcel ./src/index.html”,
“clean:dist”: “rimraf ./dist”,
“format”: “refmt —in-place ./src/*.re”,
“clean”: “npm run clean:dist && npm run clean:bsb”,
“scratch”: “npm run clean && npm run build”
},
“keywords”: [
“BuckleScript”
],
“author”: “”,
“license”: “MIT”,
“devDependencies”: {
“bs-platform”: “^4.0.7”,
“parcel”: “^1.10.3”,
“rimraf”: “^2.6.2”
},
“dependencies”: {
“bs-webapi”: “^0.13.1”
}
}

Terminal session:

webpage$ npm run scratch

> shirts@0.1.0 scratch …/reason/web_dev/webpage
> npm run clean && npm run build

> shirts@0.1.0 clean …/reason/web_dev/webpage
> npm run clean:dist && npm run clean:bsb

> shirts@0.1.0 clean:dist …/reason/web_dev/webpage
> rimraf ./dist

> shirts@0.1.0 clean:bsb …/reason/web_dev/webpage
> bsb -clean-world

Cleaning… 410 files.
Cleaning… 6 files.

> shirts@0.1.0 build …/reason/web_dev/webpage
> bsb -make-world

[164/164] Building src/core/Base64Re.mlast.d
[82/82] Building src/Webapi.cmj
[3/3] Building src/WebShirts.mlast.d
[1/1] Building src/WebShirts-Shirts.cmj
webpage$ open -a Terminal “`pwd`”
webpage$ npm run watch:parcel &
[1] 3608
> shirts@0.1.0 watch:parcel …/reason/web_dev/webpage
> parcel ./src/index.html

Server running at localhost:1234
✨ Built in 699ms.

webpage$ jobs
[1]+ Running npm run watch:parcel &
webpage$ fg
npm run watch:parcel
^C
webpage$ jobs
webpage$

The second terminal is necessary for bsb to watch:

webpage$ npm run watch:bsb

> shirts@0.1.0 watch:bsb /Users/wheatley/sbox/reason/web_dev/webpage
> bsb -w

>>>> Start compiling
[1/1] Building Shirts.cmi
ninja: no work to do.
>>>> Finish compiling

BuckleScript Github Issue #2750 "bsb -make-world w exits early when backgrounded" outlines that bsb will currently exit when it is run in the background so the watch process will need it’s own terminal.

2018-12-05Leaving things as they are.
63SUGGEST

collections/src/ListExamples.re

let badElement = Belt.List.getExn(items, 10); /* throws error */

It may be a bit more instructive to see:

let badElement: int =
try (Belt.List.getExn(items, 10)) {
| Js.Exn.Error(e) =>
switch (Js.Exn.message(e)) {
| Some(message) => Js.log({j|Error: $message|j}) /* output: “Error: getExn”*/
| None => Js.log(“An unknown error occurred”)
};
(–1);
};

with a reference to the section discussing exception handling.

2018-12-04Adding some verbiage to show this other way of handling errors.
65SUGGEST

Please excuse the following rant - there is a point, though likely it’s not going to impact the text referenced:

“The body of isMedium() is only one line, so this is a case where it might be better to express it as an anonymous function.”

While this is a rampant attitude in the JavaScript community:

“Any fool can write code that a computer can understand. Good programmers write code that humans can understand.”
- p. 15, Refactoring: Improving the design of existing code; 1999

  • An inline anonymous function is pre-occupied with “what needs to be done” - the reader needs to parse the code to determine its actual, higher level intent.
  • A well named function clearly communicates its intent. Knowing the intent beforehand makes it easier for the reader to parse the code to verify the established expectation. After that the details of the code can be dispensed with and the name becomes a sufficiently descriptive “handle” for the remainder of the code (even if it is only referenced once).

In many cases the terseness of inline, anonymous functions actually hinders readability.

In this particular case

size === ShirtSize.Medium

communicates the intent well enough but given that we need

((_, size)) => size === ShirtSize.Medium

anyway, the savings seem hardly worth it. And ultimately

let mediums = Belt.List.keep(orderList, isMedium);

is easier to read than

let mediums2 =
Belt.List.keep(orderList, ((_, size)) => size === ShirtSize.Medium);

but I guess that is subjective.

The primary value of an anonymous function (or any other function created or defined in a non-global scope) is in it’s closure (i.e. its connection to the scope that created it). And if that anonymous function gets too long, it makes sense to create a well named function that creates it while providing the necessary scope through it’s arguments.

2018-12-05Added some verbiage about how you will be reading code more often than the computer will, so if readability is paramount, you should use named functions no matter how short they are. Also changed a couple of examples in this chapter to use named functions
65SUGGEST

“people think of things being filtered out”

You filter out the gold because you want keep it; that being said we are surrounded by filters that keep undesirables and pass desirables rather than keeping desirables and passing undesirables. Given the mental model of a pipelined value (or a pipeline of values in the case of a stream) the predicate supplied to “filter” focuses on values being PASSED, not the ones that are being discarded (i.e. filtered out).

Furthermore looking at the Belt.List documentation:

keep xs p returns a list of all elements in xs which satisfy the predicate function p
filter xs p returns a list of all elements in xs which satisfy the predicate function p
keepWithIndex xs p returns a list of all elements in xs which satisfy the predicate function p
filterWithIndex xs p returns a list of all elements in xs which satisfy the predicate function p

val keepU : ’a t -> (’a -> bool [bs]) -> 'a t val keepWithIndexU : 'a t -> ('a -> int -> bool [bs]) -> ’a t
val keepMapU : ’a t -> (’a -> ’b option [@bs]) -> ’b t
keepMap xs f applies f to each element of xs. If f xi returns Some value, then value is kept in the resulting list; if f xi returns None, the element is not retained in the result.

So the “keep” functions are of primary interest when using uncurried JS functions as predicates.

Given that “OCaml Batteries” includes BatList.filter_map, Belt.List.filterMap may not be that far behind.

2018-11-26The source for Belt.List.filter at https://github.com/BuckleScript/bucklescript/blob/master/jscomp/others/belt_List.mli#L578 says: @@deprecated "This function will soon be deprecated. Please, use `List.keep` instead." (same for filterWithIndex)
66SUGGEST

let addPrice = (accumulator, orderItem) => accumulator +. orderPrice(orderItem);

Instead of “accumulator” wouldn’t something like “currentTotal”, “runningTotal”, or “tally” be clearer?
Unless the entire point is to highlight the more generic concept of an “accumulator”.
While it’s passable in an API document, I generally don’t find “accumulator” to be a very helpful identifier name and many people have a very narrow interpretation of what it means.

let finalState = Belt.List.reduce(stateChanges, initialState, reducer);

Furthermore the narrative of the description would be easier to follow if there was a logged version of the reducer:

let addPriceLogged = (currentTotal, order) => {
let price = priceOrder(order);
Js.log({j|$currentTotal $price|j});
currentTotal + price;
};

let addPrice = (currentTotal, order) => currentTotal + priceOrder(order);

let totalPrice = Belt.List.reduce(orders, 0, addPriceLogged);

0 8750
8750 8500
17250 4400
21650 8400
30050 8800
38850 2800
41650 11250
52900 4950
Total price: 57850

2018-11-26I am making the name change; not sure about adding the Js.log stuff. Let me give that some thought.
68SUGGEST

collections/src/MapKeepReduce.re

readability

let mediumTotal =
Belt.List.keep(orders, isMedium)
->Belt.List.map(priceOrder)
->Belt.List.reduce(0, (+)); /* i.e. sum all order prices */

Js.log2(“Medium total:”, mediumTotal); /* Medium total: 20000 */

The /* i.e. sum all order prices */ comment is really only necessary for the uninitiated. Strangely enough that type of usage tends to stick pretty quickly - probably because of the massive double take that is experienced the FIRST time one encounters it. One of the few times where “unfamiliarity” is actually helpful.

2018-11-27Keeping this as is, because I am presuming most of the readers will be the uninitiated.
70SUGGEST

Interlude: Displaying Lists

I would expect that in this case “JavaScript Developers” would look for an equivalent to Array.prototype.join() which is roughly mirrored for lists with String.concat: (string, list(string)) => string;

let stringOfList2: (list(’a), ’a => string) => string =
(values, stringify) => {
let str = valuesBelt.List.map(stringify)>String.concat(“, ”, _);
“[” str “]”;
};

Js.log(stringOfList2(intValues, string_of_int)); /* [10, 11, 12, 13, 14, 15] */
Js.log(stringOfList2([], string_of_int)); /* [] */

But then again maybe you are deliberately avoiding OCaml’s standard library in favour of BuckleScript centric libraries like Belt and Js.

2018-11-27Yes, I am staying with Belt as much as possible, then Js.
73TYPO

collections-app/src/index.html

<p>Shirt Price Calcuator</p>

should be

<p>Shirt Price Calculator</p>
2018-11-26Fixed; thanks.
75SUGGEST

collections-app/src/OrderPage.re

Just a heads up

let pattern = [%re “/\\\\s*,\\\\s*/”];

the reverse solidus (backslash) can be easily confused with a broken bar (pipe) character as the slant is minimal in the text of the PDF. But the text of course does mention “backslashes” specifically.

Consider using quoted strings instead (to eliminate the escape backslashes):

let commaSplit: string => array(string) =
s => {
let pattern = [%bs.re {|/\\s*,\\s*/|}];
Js.String.splitByRe(pattern, s);
};

Also

let commaSplit2: string => array(string) =
s => Js.String.split(“,”, s)->Belt.Array.map(Js.String.trim);

seems more straightforward though I imagine this is meant as a warmup to using regular expressions.

2018-12-04Added verbiage about {|...|} but am leaving it as a regex.
75SUGGEST

collections-app/src/OrderPage.re

readability

type order = (int, ShirtSize.t);
type t = Belt.Result.t(order, string);

let makeOrderError: string => t = input => Belt.Result.Error(input);

let makeOrderResult: (option(ShirtSize.t), array(string), string) => t =
(maybeSize, captures, input) =>
switch (maybeSize) {
| Some(size) => Belt.Result.Ok((int_of_string(captures[1]), size))
| None => makeOrderError(input)
};

let orderOfCaptures: (option(array(string)), string) => t =
(maybeCaptures, input) =>
switch (maybeCaptures) {
| Some(captures) =>
ShirtSize.fromString(captures[2])->makeOrderResult(captures, input)
| None => makeOrderError(input)
};

let toOrder: string => t =
input =>
Js.String.toUpperCase(input)
->Js.String.match([%re {|/(\\d{1,4})\\s@?\\s(S|M|X{0,4}L)/|}], _)
->orderOfCaptures(input);

let orderPrice: order => int = ((n, size)) => n * ShirtSize.price(size);

let addOrderTotal: ((int, int), t) => (int, int) =
((count, total) as current, orderResult) =>
switch (orderResult) {
| Belt.Result.Ok((n, _) as order) => (
count + n,
total + orderPrice(order),
)
| _ => current
};

let aggregateOrderTotals: array(t) => (int, int) =
orders => Belt.Array.reduce(orders, (0, 0), addOrderTotal);

let toBasic: int => float = fractional => float_of_int(fractional) /. 100.0;

let makeOrderRow: order => string =
((n, size) as order) => {
let totalPrice =
orderPrice(order)toBasic>Js.Float.toFixedWithPrecision(~digits=2);
let shirtSize = ShirtSize.toString(size);
{j|

$n

$shirtSize

\\$$totalPrice

Bad input $input

|j};

let tableBottom = {j|

|j};

let appendResultRow: (string, t) => string =
(rows, orderResult) => rows createRow(orderResult);

let createTable: array(t) => string =
orders =>
tableTop Belt.Array.reduce(orders, “”, appendResultRow) tableBottom;

let getValueById: string => option(string) =
id =>
Doc.getElementById(id, D.document)
->Belt.Option.map(Elem.unsafeAsHtmlElement)
->Belt.Option.map(D.HtmlElement.value);

let setById: ((string, Elem.t) => unit, string, string) => unit =
(set, id, text) =>
Doc.getElementById(id, D.document)Belt.Option.map(set(text))>ignore;

let setInnerHTML: (string, Elem.t) => unit =
(markup, elem) => Elem.setInnerHTML(elem, markup);

let setTextContent: (string, Elem.t) => unit =
(text, elem) => Elem.setTextContent(elem, text);

let setInnerHTMLById: (string, string) => unit =
(id, markup) => setById(setInnerHTML, id, markup);

let setTextContentById: (string, string) => unit =
(id, text) => setById(setTextContent, id, text);

let calculate: Dom.event => unit =
_ =>
switch (getValueById(“orders”)) {
| Some(ordersInput) =>
let orders =
commaSplit(ordersInput)
->Belt.Array.keep((!==)(“”))
->Belt.Array.map(toOrder);
let (count, total) = aggregateOrderTotals(orders);
let totalPrice =
toBasic(total)->Js.Float.toFixedWithPrecision(~digits=2);
let _ = setInnerHTMLById(“table”, createTable(orders));
let _ = setTextContentById(“totalShirts”, string_of_int(count));
let _ = setTextContentById(“totalPrice”, totalPrice);
();
| None => ()
};

let calcButton = Doc.getElementById(“calculate”, D.document);

switch (calcButton) {
| Some(element) =>
D.EventTarget.addEventListener(
“click”,
calculate,
Elem.asEventTarget(element),
)
| None => ()
};

2018-12-04Taking some of the suggested changes, but not all of them. 77SUGGEST

Variable name “optOrder” - value is actually a Belt.Result.t, not a Belt.Option.t.

2018-11-26Renamed it "resOrder" so as not to conflict with "resultOrder". 88SUGGEST

One has to look in “recursion/palindrome/src/Palindrome.re” to discover “module Arr = Belt.Array;” in order to confirm what “Arr” in the text’s code listing is.

This can be a bit confusing as in Chapter 5 the fully qualified names were used consistently.

2018-12-04Since doing fully qualified names happens only once or twice, I have put in the FQN instead. 89SUGGEST

“Note that the repeatHelper() function doesn’t need to have s as one of its parameters because that value is available within the enclosing braces.”

Not sure how helpful this statement is for someone without preexisting understanding of the concept of a closure. JavaScript programmers are supposed to be aware of closures. I’d stick with

“Note that the repeatHelper() function doesn’t need to have s as one of its parameters because that value is available within it’s closure.”

If somebody doesn’t understand it, they can search online for “closure” or perhaps include in the text a link to the MDN “Closures” page.

As it is, in the text of the code “s” doesn’t actually appear “within the enclosing braces” but in the parameter list that is part of the function definition which makes it part of the scope that is implied by the braces.

2018-12-04Fixed end of sentence to read "...is available within repeatHelper()'s scope." I gave the name of the method so that I wouldn’t have a vague pronoun reference with “its”. 89SUGGEST

While “Js.Date.now()” works, it seems that “performance.now()” would be more suited to the task.

For the sake of the discussion “Js.Date.now()” does keep things less cluttered but a “performance.now()” version in the source code bundle could be used to “show off the coming attractions” (modules and JavaScript interop) of upcoming chapters.

module Performance = {
type t; /* DOMHighResTimeStamp - a double, milliseconds since startup */

[bs.scope "performance"] [bs.module “perf_hooks”]
external now: unit => t = “”;

external toFloat: t => float = “%identity”;
external fromFloat: float => t = “%identity”;

let zero: t = fromFloat(0.0);

let addInterval: (t, t, t) => t =
(current, start, finish) =>
(toFloat(finish) . toFloat(start) +. toFloat(current))>fromFloat;
};

let rec isPalindrome: string => bool =
s => {
let len = Js.String.length(s);
if (len <= 1) {
true;
} else if (Js.String.get(s, 0) != Js.String.get(s, len - 1)) {
false;
} else {
isPalindrome(Js.String.slice(~from=1, ~to_=len - 1, s));
};
};

let rec isPalindrome2Helper: (string, int, int) => bool =
(s, lower, upper) =>
if (upper - lower < 1) {
true;
} else if (Js.String.get(s, lower) != Js.String.get(s, upper)) {
false;
} else {
isPalindrome2Helper(s, lower + 1, upper - 1);
};

let isPalindrome2: string => bool =
s => isPalindrome2Helper(s, 0, Js.String.length(s) - 1);

let isPalindrome3: string => bool =
s => {
let result = ref(None);
let upper = ref(Js.String.length(s) - 1);
let lower = ref(0);

while (Belt.Option.isNone(result^)) {
if (upper^ - lower^ < 1) {
result := Some(true);
} else if (Js.String.get(s, lower) != Js.String.get(s, upper)) {
result := Some(false);
} else {
lower := lower^ + 1;
upper := upper^ - 1;
};
};

Belt.Option.getWithDefault(result^, false);
};

let repeat: (string, int) => string =
(s, n) => {
let rec repeatHelper: (string, int) => string =
(current, counter) =>
switch (counter) {
| 0 => current
| _ => repeatHelper(current s, counter - 1)
};
repeatHelper(“”, n);
};

let testString: string = repeat(“a”, 50000);
let rec repeatTest: (string => bool, int, Performance.t) => Performance.t =
(func, n, current) =>
switch (n) {
| 0 => current
| _ =>
let start = Performance.now();
let _ = func(testString);
let finish = Performance.now();
repeatTest(
func,
n - 1,
Performance.addInterval(current, start, finish),
);
};

let run: (string => bool, int) => string =
(func, n) =>
repeatTest(func, n, Performance.zero)
->Performance.toFloat
->((/.)(1000.0))
->Js.Float.toFixedWithPrecision(~digits=3);

let funcs: array(string => bool) = [|
isPalindrome,
isPalindrome2,
isPalindrome3,
|];
let results: array(string) = Belt.Array.map(funcs, func => run(func, 1000));

Js.log2(“Average time in msec:”, Js.Array.joinWith(" ", results));

2018-12-04I do not want to give that much of a preview. There would be a lot to explain, and I don’t want to present it as a magical incantation. However, it might make a good exercise for the Interop chapter, if I can put it somewhere without breaking the narrative flow. 91SUGGEST

“No extra storage space is needed, and ReasonML will optimize the tail recursion into a JavaScript while loop, so the code avoids stack overflow.”

Encourage the reader to actually look at the code (Repeat.bs.js) that is generated by the compiler for “Repeat.re”.

Possibly juxtapose the tail recursive version with a non-recursive version using a while loop (with forward references to the explanation of “ref”s if necessary).

ReasonML:

let rec repeatRec: (string, int, string) => string =
(s, n, current) =>
switch (n) {
| 0 => current
| _ => repeatRec(s, n - 1, current s)
};

let repeatRec3: (string, int) => string =
(s, m) => {
let current = ref(“”);
let n = ref(m);
while (n^ > 0) {
current := current^ s;
n := n^ - 1;
};
current^;
};

JavaScript:

function repeatRec(s, _n, _current) {
while(true) {
var current = _current;
var n = _n;
if (n !== 0) {
_current = current + s;
_n = n - 1 | 0;
continue ;
} else {
return current;
}
};
}

function repeatRec3(s, m) {
var current = “”;
var n = m;
while(n > 0) {
current = current + s;
n = n - 1 | 0;
};
return current;

2018-12-04Will add verbiage about looking at the generated code. Not going to do refs, sorry. 92SUGGEST

recursion/keep-indices/src/WithoutHelper.re

FYI: refmt will transform the “switch on boolean expression” to the ternary operator.

2018-12-04Changed it to a ternary expression in the source. 93SUGGEST

recursion/keep-indices/src/WithHelper.re

IF you are trying to make a point maybe rearrange the parameter order for both examples (i.e. compare explicit function type declared for “keepIndices” and compare the number of defined parameter names):

let keepIndices: (array(’a), ’a => bool, int, array(int)) => array(int) =
(arr, f) => {
let rec helper: (int, array(int)) => array(int) =
(position, current) =>
if (position < Belt.Array.length(arr)) {
f(Belt.Array.getUnsafe(arr, position)) ?
helper(position + 1, Belt.Array.concat(current, [|position|])) :
helper(position + 1, current);
} else {
current;
};
helper;
};

let words = [|“cow”, “aardvark”, “squirrel”, “fish”, “snake”, “capybara”|];
let isShortWord: string => bool = s => Js.String.length(s) < 6;
let result = keepIndices(words, isShortWord, 0, [||]);
Js.log(result);

2018-12-05Changed switches to ternary operators; not sure what other change is here... 94SUGGEST

recursion/display-list/src/DisplayList.re

let makeHelper: (’a => string, string, list(’a)) => string =
stringify => {
let next = (current, x) => current stringify(x);
let rec helper: (string, list(’a)) => string =
(current, rest) =>
switch (rest) {
| [x] => next(current, x)
| [x, …xs] => helper(next(current, x) “, ”, xs)
| _ => current /* never gonna happen */
};
helper;
};

let stringOfList: (list(’a), ’a => string) => string =
(items, stringify) =>
switch (items) {
| [] => “”
| _ => (makeHelper(stringify))(“”, items)
};

let items = [10, 11, 12, 13, 14, 15];
let floatItems = [3.6, 7.9, 8.25, 41.0];

Js.log(stringOfList(items, string_of_int));
Js.log(stringOfList(floatItems, string_of_float));

2019-08-07Keeping as is. 97TYPO

“You’ll this in action as we create custom modules that serve as arguments to other modules.”

Possibly

“You’ll see this …”

2018-12-04I think I caught this on a read-through; should be fixed in next version. 99TYPO

records/shirts/src/Shirts.re

Js.log2(“Size:”, myOrder.size); /* 80.0 */

should be

Js.log2(“Size:”, myOrder.size); /* Size: [ 1, tag: 1 ] */

and

Js.log2(“Cuff:”, otherOrder.cuff); /* French */

should be

Js.log2(“Cuff:”, otherOrder.cuff); /* Cuff: 1 */

2018-12-04Will fix; thanks. 100SUGGEST

records/mod-shirts/src/Shirts.re

With all those “toString” functions you have the opportunity to introduce the “fun” short form for the “switch” expression, e.g.:

let toString: t => string =
fun
| XSmall(n) => Js.String.repeat(n, “X”) “S”
| Small => “S”
| Medium => “M”
| Large => “L”
| XLarge(n) => Js.String.repeat(n, “X”) “L”;

let toString: t => string =
fun
| Button => “button”
| French => “french”
| NoCuff => “none”;

FYI:

String.make(n, ‘X’)

could use

Js.String.repeat(n,“X”)

2018-12-05I'm putting that in Chapter 10. 104TYPO

“You can see a file containing 100 orders at code/records/shirt-stats/orders.csv and a smaller test file of six orders at code/records/shirt-stats/mini-orders.csv.”

There only seems to be an “orders.csv”" file under “code/interop” - there don’t seem to be any other “.csv” files.

2018-12-04Will upload these in the next SVN commit. 105SUGGEST

records/shirt-stats/src/Stats.re

Current code uses classic style pipe (not fast pipe) and Js.Array.sliceFrom rather than Belt.Array.sliceToEnd

let processFile: string => unit =
inFileName => {
let fileContents = Node.Fs.readFileAsUtf8Sync(inFileName);
let lines =
Js.String.split(“\
”, fileContents)->Belt.Array.sliceToEnd(1);
let orders = Belt.Array.reduce(lines, [], lineReducer);
printStatistics(orders);
};

2018-12-05Will fix; thanks. 106SUGGEST

records/shirt-stats/src/Stats.re

Consider breaking “lineReducer” down even further for clarity, e.g.:

type order = {
quantity: int,
size: Size.t,
sleeve: Sleeve.t,
color: Color.t,
pattern: Pattern.t,
cuff: Cuff.t,
collar: Collar.t,
};

let setOrderQuantity: (order, int) => order =
(order, quantity) => {…order, quantity};
let setOrderSize: (order, Size.t) => order =
(order, size) => {…order, size};
let setOrderSleeve: (order, Sleeve.t) => order =
(order, sleeve) => {…order, sleeve};
let setOrderColor: (order, Color.t) => order =
(order, color) => {…order, color};
let setOrderPattern: (order, Pattern.t) => order =
(order, pattern) => {…order, pattern};
let setOrderCuff: (order, Cuff.t) => order =
(order, cuff) => {…order, cuff};
let setOrderCollar: (order, Collar.t) => order =
(order, collar) => {…order, collar};

let map2: (option(’a), option(’b), (’a, ’b) => ’c) => option(’c) =
(maybe1, maybe2, f2) =>
switch (maybe1, maybe2) {
| (Some(arg1), Some(arg2)) => Some(f2(arg1, arg2))
| (,) => None
};

let maybeOrderFromItems: array(string) => option(order) =
items => {
let emptyOrder =
Some({
quantity: 0,
size: Small,
sleeve: Short,
color: White,
pattern: Solid,
cuff: Button,
collar: Straight,
});

map2(emptyOrder, maybeIntFromString(items[0]), setOrderQuantity)
->map2(Size.fromString(items[1]), setOrderSize)
->map2(Color.fromString(items[2]), setOrderColor)
->map2(Pattern.fromString(items[3]), setOrderPattern)
->map2(Collar.fromString(items[4]), setOrderCollar)
->map2(Sleeve.fromString(items[5]), setOrderSleeve)
->map2(Cuff.fromString(items[6]), setOrderCuff);
};

let lineReducer: (list(order), string) => list(order) =
(orders, line) => {
let items = Js.String.split(“,”, line);
Belt.Array.length(items) != 7 ?
orders :
maybeOrderFromItems(items)
->Belt.Option.mapWithDefault(orders, order => [order, …orders]);
};

2019-08-07Keeping as is. 109SUGGEST

records/shirt-stats/src/Stats.re

You have an opportunity to introduce “Belt.Map.update(m,k,f)”, e.g.:

type colorCountMap =
Belt.Map.t(ColorComparator.t, int, ColorComparator.identity);

let addOrderQuantity: (order, option(int)) => option(int) =
order =>
fun
| Some(n) => Some(n + order.quantity)
| None => Some(order.quantity);

let colorCounter: (colorCountMap, order) => colorCountMap =
(counts, order) =>
Belt.Map.update(counts, order.color, addOrderQuantity(order));

let logColorCount: (Color.t, int) => unit =
(key, value) => Js.log2(Color.toString(key), value);

let printStatistics: list(order) => unit =
orders => {
let emptyColors = Belt.Map.make(~id=(module ColorComparator));
let colorDistribution =
Belt.List.reduce(orders, emptyColors, colorCounter);
Js.log2(“Color”, “Quantity”);
Belt.Map.forEach(colorDistribution, logColorCount);
};

2018-12-05Put in the type and pulled out the reducer function as separate, but did not use Belt.Map.update(). 111TYPO

“A module within a file cancontain anything that you would put in a ReasonML file—for example, type definitions, function defintions, and even other modules”

should be:

“A module within a file >can contain< … function >definitions<, and even other modules”

2018-12-03 115SUGGEST

Given how Stats.re is used in the next chapter (one distribution/count at a time ) makes the following suggestion much more tangential than I initially thought. But here it is as I had already captured it:

“It’s your turn”

By imposing the constraint to collect all distributions in the same reduce pass additional opportunity is given to:

e.g.

type sizeCountMap = Belt.Map.t(Size.t, int, Size.Comparator.identity);
type colorCountMap = Belt.Map.t(Color.t, int, Color.Comparator.identity);
type sleeveCountMap = Belt.Map.t(Sleeve.t, int, Sleeve.Comparator.identity);
type patternCountMap =
Belt.Map.t(Pattern.t, int, Pattern.Comparator.identity);
type collarCountMap = Belt.Map.t(Collar.t, int, Collar.Comparator.identity);
type cuffCountMap = Belt.Map.t(Cuff.t, int, Cuff.Comparator.identity);

type aspectCounts = {
sizes: sizeCountMap,
colors: colorCountMap,
sleeves: sleeveCountMap,
patterns: patternCountMap,
collars: collarCountMap,
cuffs: cuffCountMap,
};

let makeEmptyCounts: unit => aspectCounts =
() => {
sizes: Belt.Map.make(~id=(module Size.Comparator)),
colors: Belt.Map.make(~id=(module Color.Comparator)),
sleeves: Belt.Map.make(~id=(module Sleeve.Comparator)),
patterns: Belt.Map.make(~id=(module Pattern.Comparator)),
collars: Belt.Map.make(~id=(module Collar.Comparator)),
cuffs: Belt.Map.make(~id=(module Cuff.Comparator)),
};

let setSizesCount: (aspectCounts, sizeCountMap) => aspectCounts =
(counts, sizes) => {…counts, sizes};
let setColorsCount: (aspectCounts, colorCountMap) => aspectCounts =
(counts, colors) => {…counts, colors};
let setSleevesCount: (aspectCounts, sleeveCountMap) => aspectCounts =
(counts, sleeves) => {…counts, sleeves};
let setPatternsCount: (aspectCounts, patternCountMap) => aspectCounts =
(counts, patterns) => {…counts, patterns};
let setCollarsCount: (aspectCounts, collarCountMap) => aspectCounts =
(counts, collars) => {…counts, collars};
let setCuffsCount: (aspectCounts, cuffCountMap) => aspectCounts =
(counts, cuffs) => {…counts, cuffs};

let apply2: (’a, ’b, (’a, ’b) => ’c) => ’c = (a1, a2, f) => f(a1, a2);

let increment: (int, option(int)) => option(int) =
quantity =>
fun
| Some(n) => Some(n + quantity)
| None => Some(quantity);

let aspectCounter: (aspectCounts, order) => aspectCounts =
(counts, order) => {
let addQuantity = increment(order.quantity);
apply2(
counts,
Belt.Map.update(counts.sizes, order.size, addQuantity),
setSizesCount,
)
->apply2(
Belt.Map.update(counts.colors, order.color, addQuantity),
setColorsCount,
)
->apply2(
Belt.Map.update(counts.sleeves, order.sleeve, addQuantity),
setSleevesCount,
)
->apply2(
Belt.Map.update(counts.patterns, order.pattern, addQuantity),
setPatternsCount,
)
->apply2(
Belt.Map.update(counts.collars, order.collar, addQuantity),
setCollarsCount,
)
->apply2(
Belt.Map.update(counts.cuffs, order.cuff, addQuantity),
setCuffsCount,
);
};

let printEntries: (string, ’k => string, Belt.Map.t(’k, int, ’id)) => unit =
(title, toString, countMap) => {
let printEntry: (’k, int) => unit =
(key, value) => Js.log2(toString(key), value);
Js.log2(title, “Quantity”);
Belt.Map.forEach(countMap, printEntry);
};

let printStatistics: list(order) => unit =
orders => {
let counts = Belt.List.reduce(orders, makeEmptyCounts(), aspectCounter);
printEntries(“Size”, Size.toString, counts.sizes);
printEntries(“Color”, Color.toString, counts.colors);
printEntries(“Sleeve”, Sleeve.toString, counts.sleeves);
printEntries(“Pattern”, Pattern.toString, counts.patterns);
printEntries(“Collar”, Collar.toString, counts.collars);
printEntries(“Cuff”, Cuff.toString, counts.cuffs);
};

2018-12-05Since it is tangential, I am going to leave things as they are. 122SUGGEST

“Put in generic terms: [@bs.send] says that a JavaScript call of the form someObject.method(arguments) is written in ReasonML as method(someObject, arguments)”

While convenient for the sake of illustration “method(someObject, arguments)” tends to create terminology problems for people with an OO background who start to use method (which by definition has to be attached to an object) when they mean function (possibly targeted to a specific data type).

From that perspective sticking to something like “someObject.name(…theArgs)” and “name(someObject, …theArgs)” may be preferable (“…” here representing the rest parameter syntax rather than the spread syntax).

2018-12-05Change to: someObject.name(arg1, arg2, ...) is written in ReasonML as name(someObject, arg1, arg2, ...) 123SUGGEST

let dataList = [1, 2, 3, 4];
let dataArray = Array.of_list(dataList);
let newList = Array.to_list(dataArray);

? maybe instead?

let dataList = [1, 2, 3, 4];
let dataArray = Belt.List.toArray(dataList);
let newList = Belt.List.fromArray(dataArray);

2018-12-05Good idea; will make that change. 126SUGGEST

“Look for sections marked TODO: for instructions.”

The prose of the last two TODOs:

really doesn’t seem that helpful in providing the information that is actually needed to complete the bindings.

This is further complicated by the fact that the Papa Parse documentation and samples use the “Papa” module name - NOT the node module name “papaparse”.

The typical approach would involve assembling some minimal working JavaScript code to validate one’s understanding of the library and then translate that knowledge to ReasonML. So it would be more helpful to have a working JavaScript sample to work off of, e.g.:

// file: src/sample.js
// use: $ node ./src/sample.js order.csv
//
“use strict”
const fs = require(“fs”)
const process = require(“process”)
const papaparse = require(“papaparse”) /* import papaparse from “papaparse” — for ES2015 modules and bundlers */

/* Create a binding named parseString that calls
the parse() function in PapaParse.
*/

let parseString = papaparse.parse

/*
The CSV content to be parsed is passed as a string argument.
It returns an object containing the parsing “results”:

{
data: // array of parsed data
errors: // array of errors
meta: // object with extra info
}

IF the content DOES NOT begin with a header row
then “data” is an array of rows.
Each row is an array of strings which
holds the values of the CSV fields in order.

IF the first row IS a a header row
then “data” is an array of objects.
Each object represents a single row.
Each field of a row is captured by a single
object property.
The property name identifies the field name
and the property value is the field value.

Schema for a single “error” object:

{
type: “”, // A generalization of the error
code: “”, // Standardized error code
message: “”, // Human-readable details
row: 0, // Row index of parsed data where error is
}

Schema of the “meta” object:

{
delimiter: // Delimiter used
linebreak: // Line break sequence used
aborted: // Whether process was aborted
fields: // Array of field names
truncated: // Whether preview consumed all input
}

*/

function lineReducer(orders, line) {
const length = line.length
if (length === 7) {
orders.unshift(length)
}
return orders
}

function printStatistics(orders) {
console.log(orders.length)
}

function processFile(inFileName) {
const fileContents = fs.readFileSync(inFileName, “utf8”)
const parseData = parseString(fileContents).data /* i.e. parse the data */
const lines = parseData.slice(1)
const orders = lines.reduce(lineReducer, [])
printStatistics(orders)
}

const nodeArg = process.argv[0]
const progArg = process.argv[1]
const fileArg = process.argv[2]

if (fileArg) {
processFile(fileArg)
} else if (nodeArg && progArg) {
console.log(“Usage: ” + (nodeArg + (" " + (progArg + " inputfile.csv“))))
} else {
console.log(”How did you get here without NodeJS or a program to run?")
}

2018-12-05Gave a more complete description of what is necessary in the first TODO, including the module name and return type. 127TYPO

interop/json-example/src/JsonExample.re

e.g.

module D = Json.Decode;

let stringDecoder = D.string;
let decodedString =
Json.parse({js|“two words”|js})
->Belt.Option.mapWithDefault(“”, stringDecoder);

Js.log(decodedString); /* two words */

/* Compose array decoder with float decoder with partial application */
let floatArrayDecoder = D.array(D.float);
let decodedArray =
Json.parse(“[3.4, 5.6, 7.8]”)
->Belt.Option.mapWithDefault([||], floatArrayDecoder);

Js.log(decodedArray); /* [ 3.4, 5.6, 7.8 ] */

/* Again compose decoder for later use */
let qtyPropDecoder = D.field(“qty”, D.int);
let qty =
Json.parse({|{“size”: “XXL”, “qty”: 10}|})
->Belt.Option.mapWithDefault(0, qtyPropDecoder);

Js.log(qty); /* 10 */

/* OR */

module D = Json.Decode;

let stringDecoder = D.string;
let decodedString =
switch (Json.parse({js|“two words”|js})) {
| Some(jsonStr) => stringDecoder(jsonStr)
| None => “”
};

Js.log(decodedString); /* two words */

/* Compose array decoder with float decoder through partial application */
let floatArrayDecoder = D.array(D.float);
let decodedArray =
switch (Json.parse(“[3.4, 5.6, 7.8]”)) {
| Some(jsonArray) => floatArrayDecoder(jsonArray)
| None => [||]
};

Js.log(decodedArray); /* [ 3.4, 5.6, 7.8 ] */

/* Again compose decoder for later use */
let qtyPropDecoder = D.field(“qty”, D.int);
let qty =
switch (Json.parse({|{“size”: “XXL”, “qty”: 10}|})) {
| Some(jsonObj) => qtyPropDecoder(jsonObj)
| None => 0
};

Js.log(qty); /* 10 */

2018-12-05Changed to decodedArray, decodedStr, etc. Also used the composition on the array example (but not the object example). 129SUGGEST

code/interop/server/src/Server.re

1. It seems odd to start the server listening before the routes are added. I would have expected

let server = Express.App.listen(app, ~port=3000, ~onListen, ());

to be the last line of the script. But I’m no Express expert (online search: expressjs hello-world).

2. The code seems strangely JavaScript-y, not OCaml/BuckleScript/ReasonML-y - possibly because of the total absence of pipes - e.g.:

let onListen: Js.nullable(Js.Exn.t) => unit =
e =>
switch (e) {
| exception (Js.Exn.Error(e)) =>
Js.log(e);
Node.Process.exit(1);
| _ => Js.log @@ “Listening at hhhh://127.0.0.1:3000”
}; /* hhhh IS http to bypass spam filter */

let app: Express.App.t = Express.express();

[@bs.deriving abstract]
type options = {root: string};

let indexHandler:
(Express.Middleware.next, Express.Request.t, Express.Response.t) =>
Express.complete =
(_next, _req) =>
Express.Response.sendFile(“index.html”, options(~root=“./dist”));

let jsonHandler:
(Express.Middleware.next, Express.Request.t, Express.Response.t) =>
Express.complete =
(_, req) =>
Express.Request.query(req)
->Js.Dict.unsafeGet(“choice”)
->Json.Decode.string
->Stats.processFile(“orders.csv”, _)
->Express.Response.sendJson;

let fileHandler:
(Express.Middleware.next, Express.Request.t, Express.Response.t) =>
Express.complete =
(_, req) =>
Express.Request.params(req)
->Js.Dict.unsafeGet(“filename”)
->Json.Decode.string
->Express.Response.sendFile(options(~root=“./dist”));

let appendRoute:
(
Express.App.t,
string,
(Express.Middleware.next, Express.Request.t, Express.Response.t) =>
Express.complete
) =>
unit =
(app, path, handler) =>
Express.Middleware.from(handler)->Express.App.get(app, ~path, _);

let _ = appendRoute(app, “/”, indexHandler);
let _ = appendRoute(app, “/json”, jsonHandler);
let _ = appendRoute(app, “/:filename”, fileHandler);

let server: Express.HttpServer.t =
Express.App.listen(app, ~port=3000, ~onListen, ());

2018-12-05Changed to fast pipe style. 132SUGGEST

“You can see the full code at code/interop/server/src/Stats.re.”

I suspect that this may be glossing over a bit of a speed bump that may present itself for readers with a background primarily in dynamic languages or little exposure to parameterized types. From that perspective it may be valuable to explain how the implementation of “statisticsToJson” was arrived at - mainly because there are lots of solution approaches that are viable in JavaScript but that are stopped in their tracks (or at least require some cogent type wrangling) against BuckleScript/ReasonML’s static typing.

Back on p.115

Belt.Map.make(~id=(module ColorComparator)),

was strange enough but here we are again

makeDistro((module Shirt.ColorComparator), (anOrder) => anOrder.color), Shirt.Color.toString)

The sidebar on p.114 does mention “module as an argument” in the context of functors but there it’s typically pretty explicit:

include EventTargetRe.Impl({ type nonrec t = t; });

Meanwhile the notation of “(module ColorComparator)” seems strange given that “ColorComparator” was clearly defined as a module already. It’s not clear whether the ColorComparator module itself is passed or whether this wraps ColorComparator into yet another module.

The implementation of “make” in
BuckleScript/bucklescript/blob/master/lib/js/belt_Map.js
suggests that the module itself is passed - allowing “make” to pick off the comparison function.

2019-08-07Not sure how to approach this; keeping as is for now. 133SUGGEST

“Implementing the client”

For the time being

npm i bs-webapi@0.9.1 bs-xmlhttprequest @glennsl/bs-json -P

is necessary to avoid the

Duplicated package: bs-webapi

warning.

2018-12-05Will add this perhaps at a later time. 134SUGGEST

interop/client/src/Client.re

Consider using

Belt.Option.map(maybeValue, func)->ignore

instead of explicit “switch”, e.g.

let requestCounts: string => unit =
category => {
let method = “GET”;
let url = {j|hhhh://localhost:3000/json?choice=$category|j}; /* hhhh IS http to bypass spam filter*/
let async = true;
let xhr = XmlHttpRequest.make();
XmlHttpRequest.open_(xhr, ~method, ~url, ~async, ());
XmlHttpRequest.addEventListener(
xhr,
`load(_event => processResponse(xhr)),
);
XmlHttpRequest.send(xhr);
};

let sendRequest: Dom.event => unit =
_ =>
Doc.getElementById(“category”, D.document)
->getValue
->Belt.Option.map(category =>
category != “” ? requestCounts(category) : ()
)
->ignore;

let addCategoryListener: Elem.t => unit =
element =>
D.EventTarget.addEventListener(
“change”,
sendRequest,
D.Element.asEventTarget(element),
);

let maybeCategory = Doc.getElementById(“category”, D.document);
Belt.Option.map(maybeCategory, addCategoryListener);

2018-12-05I think this has become moot since I have moved to using bs-fetch per another erratum suggestion. 136TYPO

“HTML string with the table of results (line 26. That code uses Belt.Array.zip(), which is the opposite of the unzip() function: it takes two arrays and combines them into a single array of paired tuples).”

The closing parenthesis (for “(Line 26. ”) at the end of the sentence is missing in the text.

So we unzipped them on the server (Stats.re) so we can zip them here?

code/interop/server/src/Stats.re
/* Separate into two arrays */
let (names, totals) = Belt.Array.unzip(pairs);

code/interop/client/src/Client.re
Belt.Array.reduce(Belt.Array.zip(choices, totals), “”,

2018-12-05Will add the closing parenthesis. \nI suspect that once I had unzip, I needed a reason to introduce zip as well. 138SUGGEST

“To access the content of a ref variable, follow its name with an up-arrow (^)”

ASCII tables refer to that character as circumflex or caret. “Deferencing uses the postfix ^.”

" rather than =, which creates a binding between a name and value."

Strictly speaking that describes

let name = value;

i.e. that “=” is joined to the “let” by the hip.

2018-12-05Will change to circumflex. 140SUGGEST

“some exceptionally bright programmers have come up with an answer: functional reactive programming”

FYI:

The term “Functional Reactive Programming” (FRP) is more or less associated with Conal Elliott’s (et al) work dating back to the mid-nineties.

“Functional Reactive Programming from First Principles (2000)”

“Functional Reactive Programming, or FRP, is a general framework for programming hybrid systems in a high-level, declarative manner. The key ideas in FRP are notions of behaviors and events. Behaviors are time-varying, reactive values, while events are time-ordered sequences of discrete-time event occurrences. FRP is the essence of Fran, a domain-specific language embedded in Haskell for programming reactive animations, but FRP is now also being used in vision, robotics and other control systems application”

So ultimately

(Reactive Programming + Functional Programming) != Functional Reactive Programming

Search online for:
Manning, Matthew Podwysocki, Stephen Blackheath, Conal Elliott, “This book isn’t about true FRP”

Search online for:
Staltz, “FRP”, “RP”, Functional Programming principles, “The introduction to Reactive Programming you’ve been missing”

2nd comment

2018-12-05Changed to "reactive programming" 55SUGGEST

“The code first has to convert the generic element to an HTML element, because that’s the only DOM type that can have a value. In this code, we use the module alias Elem for Webapi.Dom.Element.” I had to re-read these sentences a few times to unpack what I think this means. First of all what is a generic element vs and HTML element? These terms haven’t been introduced before, have they? “the only DOM type that can have a value”. What do you mean? What is meant by DOM type now? (What I think this means is that types are structs with attributes and in order to get to a type that has a “value” as an attribute that we can dig out we need to typecast one generic kind of DOM element, to an HTML specific DOM element in order to pass it into a function that can give us a value, but that function only accepts type of HTML element. Perhaps if you gave some examples of non-HTML dom elements, that would help clarify). “we use module alias Elem”, where does this module alias Elem come from? Is it just a thing we get for free and the alias is already present or did you create an alias somewhere off the page that you haven’t talked about before?

2018-12-04Yes, it’s a down-casting, and I have explained it in those terms. (I had to unfortunately refer to object-oriented programming terminology, but, then again, it *is* the Document _Object_ Model.) 77TYPO

“There’s no function corresponding to Belt.List.add(), nor can you use the … notation with arrays. These would be slow operations for arrays, as it would involve reallocating the entire array. If you want to append a single value to an array (yielding a new array), make an array that contains that single value and use Belt.List.concat():” I think you mean Belt.Array.concat() not Belt.List.concat() since you are talking about Arrays, right?

2018-12-04I think you are correct; will fix that. 6758SUGGEST

(I was reading version 1 of the book but a quick look at v2 shows this still applies)

In the “It’s Your Turn” section of chapter 4, reader is asked to add “onchange” event. I tried but it wasn’t working. Had to use “change” event for use with Webapi.Dom.EventTarget.addEventListener. (even in repo on github reasonml-community/bs-webapi-incubator/blob/master/src/dom/events/EventTargetRe.re - cannot find “onchange” - sorry but cannot paste link here).

Also, the input field needs to be “oninput” event (or to work with Webapi “input”).

Also, the exercise might ask the reader to practice currying too e.g.

```
let addEventListener = Webapi.Dom.EventTarget.addEventListener;

let addEventListenerById =
(~id: string, ~eventName: string, ~cb: Dom.event => unit) => {
let optEl = Doc.getElementById(id, D.document);
switch (optEl) {
| Some(element) =>
addEventListener(eventName, cb, Elem.asEventTarget(element))
| None => ()
};
};

let addOnInputEventListenerById = addEventListenerById(~eventName=“input”);
let addOnChangeEventListenerById = addEventListenerById(~eventName=“change”);
let addClickEventListenerById = addEventListenerById(~eventName=“click”);

addOnInputEventListenerById(~id=“quantity”, ~cb=calculate);
addOnChangeEventListenerById(~id=“size”, ~cb=calculate);
addClickEventListenerById(~id=“calculate”, ~cb=calculate);
```

(will the “It’s your turn” sections have answers in the git repo? It would be a better experience if there were).

Thanks

2018-12-04I'm sticking with (on)change for the text input -- when you press ENTER or tab out of the field, it works great. \nI have done quite a few of the “It’s your turn” but may not have referenced my solution. I will reference my version of this solution and include it in the text. 142TYPO

“Once you have your components, you tell React to render them into a web pag”. pag => page

2018-12-05Thanks; will fix that. 133SUGGEST

When looking for examples of making ajax requests in reason I came across bs-fetch on the internet. Would you consider making the client example using bs-fetch instead of bs-xmlhttprequest?

2018-12-05Let me look into that. Update: I gather that bs-fetch is more modern and what all the cool kids use :) I will make the change, though it requires a bit of rewriting. 141SUGGEST

I really didn’t “get React” until I read Dan Abramov’s “React Components, Elements, and Instances – React Blog”. While somewhat dated it may still be a good footnote/reference.

2018-12-05Added link. 142SUGGEST

“That’s the idea of React in a nutshell: components are implemented with pure functions, and they are updated in reaction to events.”

The juxtapostion of “pure functions” and “they are updated” doesn’t work; functions don’t get updated - (DOM) objects do. Furthermore ReasonReact itself doesn’t support functional components; the statelessComponent can “retain props” (which a function can’t).

React itself has two types of components: class components and functional components. Functional components merely transform the props to rendered elements, so there is no updating. Class components may maintain state so they may be updated. Class components have an advantage over functional components as they can choose to suppress unnecessary renders via the shouldComponentUpdate lifecycle method where they have access to the current (old) and the next (new) props and state. Functional components only have access to the most recent props and therefore will always render. The React.PureComponent is a React.Component with a shouldComponentUpdate method that performs a shallow comparison of current and previous props and state.

As far as I’m aware ReasonReact still doesn’t support functional components (i.e. components represented by a single function) at this time. While statelessComponent doesn’t maintain “state”, it’s last props are available on “self” via “retainedProps” (retainedProps for reducerComponent are being deprecated and will have to be maintained explicitly in the “state”) for use with the lifecycle methods (that a single function doesn’t possess).

So on a conceptual level even in ReasonReact only reducerComponents are “updated”, it’s on the implementation level that statelessComponents are updated, provided they “retain props”.

2018-12-05I have avoided this minefield altogether by deleting the verbiage. 144SUGGEST

“Component1.make(~message = ”hello“, [| |])”

it may be more illustrative to show the whole thing:

ReactDOMRe.renderToElementWithId(
ReasonReact.element(
Component1.make(~message=“Hello!”,[||])
),
“index1”
);

ReactDOMRe.renderToElementWithId(
ReasonReact.element(
Component2.make(~greeting=“Hello!”,[||])
),
“index2”
);

2018-12-05Added the ReasonReact.element() call to the existing code. 145TYPO

(1) = = ===
“The leading underscore prevents “unused variable” mesages."

“>messages<”

(2) = = ===
“make() is a function that the component’s property (or properties) and an array of child components.”

“make() is a function that >takes< the component’s property (or properties) and an array of child components.”

2018-12-05Thanks; will fix. 147SUGGEST

146

(1) = = ===
“and returns the JSX to be rendered”

After the obligatory transpilation process the JSX is gone. At run-time the functions return react elements (ReasonReact.reactElement).

(2) = = ===
“The event handler in line 7, ”

Line 7 was already discussed on p.145

2018-12-05Changed the first part, deleted duplicate description. 148SUGGEST

I suspect that you may be trying to demonstrate “independent components” - I still would have kind of expected

reason-react/notices/src/index.html

<!DOCTYPE html>

<p>Notices</p>

reason-react/notices/src/Index.re

ReactDOMRe.renderToElementWithId(



,
“root”,
);

2018-12-05Leaving as is. 149SUGGEST

Just personal preference - I like my code a bit less intensely packed. :)

[@bs.val] external require: string => string = “”;

require(“../notice_icons/information.svg”);
require(“../notice_icons/warning.svg”);
require(“../notice_icons/error.svg”);

type t =
ReasonReact.componentSpec(
ReasonReact.stateless,
ReasonReact.stateless,
ReasonReact.noRetainedProps,
ReasonReact.noRetainedProps,
ReasonReact.actionless,
);

let component: t = ReasonReact.statelessComponent(“Notice”);

let blockStyle: string => ReactDOMRe.style =
color =>
ReactDOMRe.Style.make(
~color,
~clear=“left”,
~minHeight=“64px”,
~marginBottom=“0.5em”,
~width=“30%”,
~display=“flex”,
~alignItems=“center”,
~border=“1px solid black”,
(),
);

let imgStyle: ReactDOMRe.style =
ReactDOMRe.Style.make(~width=“48px”, ~float=“left”, ());

let make: (~message: string, ~color: string, ~icon: string, array(’a)) => t =
(~message, ~color, ~icon, _children) => {
let render = _self => {
let style = blockStyle(color);
let src = “notice_icons/” icon “.svg”;


{ReasonReact.string(message)}

;
};

{…component, render};
};

2018-12-05Leaving as is unless a mob of angry tech reviewers descends upon me with pitchforks and torches. :) 152SUGGEST

“we have to create the initial state of the component in line 11.”

It may make sense to point out that “initialState” is a function (rather than just a plain record value).

2018-12-05So noted. 156SUGGEST

(1) = = ===
reason-react/shirt-react/src/OrderForm.re

“let optArr = Belt.Array.map(choices,”

Given your previous habit of prefixing option types with `“opt”` I expected option values rather than option HTML elements.

(2) = = ===
The option element array needs keys otherwise this warning appears:

react.development.js:225 Warning: Each child in an array or iterator should have a unique “key” prop.

Check the render method of `OrderForm`. See fb.me/react-warning-keys for more information.
in option (created by OrderForm)
in OrderForm

So for example:

let makeOptionElement: string => ReasonReact.reactElement =
value => {
let key = value;
let name = ReasonReact.string(value);
;
};

let makeSelect:
(string, string, array(string), string, ReactEvent.Form.t => unit) =>
ReasonReact.reactElement =
(id, name, choices, value, onChange) => {
let label = ReasonReact.string(" " name “: ”);
let options =
Belt.Array.map(choices, makeOptionElement)->ReasonReact.array;


label

;
};

(3) = = ===
Possibly make more use of punning, e.g.:

/* file: shirt-react/src/OrderForm.re
to format:
shirt-react$ refmt —in-place ./src/OrderForm.re
*/
let convertWithDefault: (string, ’a, string => option(’a)) => ’a =
(str, defaultValue, convert) =>
Belt.Option.getWithDefault(convert(str), defaultValue);

let toIntWithDefault: (string, int) => int =
(s, defaultValue) =>
switch (int_of_string(s)) {
| result => result
| exception (Failure(“int_of_string”)) => defaultValue
};

type state = {
qtyStr: string,
sizeStr: string,
sleeveStr: string,
colorStr: string,
patternStr: string,
nextOrderNumber: int,
orders: array(Shirt.Order.t),
errorText: string,
};

let initialState: unit => state =
() => {
qtyStr: “1”,
sizeStr: Shirt.Size.toString(Shirt.Size.Medium),
sleeveStr: Shirt.Sleeve.toString(Shirt.Sleeve.Long),
colorStr: Shirt.Color.toString(Shirt.Color.White),
patternStr: Shirt.Pattern.toString(Shirt.Pattern.Solid),
orders: [||],
nextOrderNumber: 1,
errorText: “”,
};

type action =
| Enter(Shirt.Order.t)
| ChangeQty(string)
| ChangeSize(string)
| ChangeSleeve(string)
| ChangeColor(string)
| ChangePattern(string)
| Delete(Shirt.Order.t);

let actionEnterOrder = order => Enter(order);
let actionChangeQty = qty => ChangeQty(qty);
let actionChangeSize = size => ChangeSize(size);
let actionChangeSleeve = sleeve => ChangeSleeve(sleeve);
let actionChangeColor = color => ChangeColor(color);
let actionChangePattern = pattern => ChangePattern(pattern);
let actionDeleteOrder = order => Delete(order);

let makeOptionElement: string => ReasonReact.reactElement =
value => {
let key = value;
let name = ReasonReact.string(value);
;
};

let makeSelect:
(string, string, array(string), string, ReactEvent.Form.t => unit) =>
ReasonReact.reactElement =
(id, name, choices, value, onChange) => {
let label = ReasonReact.string(" " name “: ”);
let options =
Belt.Array.map(choices, makeOptionElement)->ReasonReact.array;


label

;
};

let createOrder: state => Shirt.Order.t =
state => {
let {colorStr, nextOrderNumber, patternStr, qtyStr, sizeStr, sleeveStr} = state;
{
orderNumber: nextOrderNumber,
quantity: toIntWithDefault(qtyStr, 0),
size:
convertWithDefault(sizeStr, Shirt.Size.Medium, Shirt.Size.fromString),
sleeve:
convertWithDefault(
sleeveStr,
Shirt.Sleeve.Long,
Shirt.Sleeve.fromString,
),
color:
convertWithDefault(
colorStr,
Shirt.Color.White,
Shirt.Color.fromString,
),
pattern:
convertWithDefault(
patternStr,
Shirt.Pattern.Solid,
Shirt.Pattern.fromString,
),
};
};

type t =
ReasonReact.componentSpec(
state,
state,
ReasonReact.noRetainedProps,
ReasonReact.noRetainedProps,
action,
);

type s = ReasonReact.self(state, ReasonReact.noRetainedProps, action);

let component = ReasonReact.reducerComponent(“OrderForm”);

let nextState: (Shirt.Order.t, state) => state =
(order, state) => {
let orders = Belt.Array.concat(state.orders, [|order|]);
let nextOrderNumber = state.nextOrderNumber + 1;
let errorText = “”;
{…state, orders, nextOrderNumber, errorText};
};

let enterOrder: (Shirt.Order.t, state) => state =
(order, state) => {
let n = toIntWithDefault(state.qtyStr, 0);
n > 0 && n <= 100 ?
nextState(order, state) :
{…state, errorText: “Quantity must be between 1 and 100”};
};

let deleteOrder: (Shirt.Order.t, state) => state =
(delete, state) => {
let keepOrder: Shirt.Order.t => bool =
order => order.orderNumber != delete.orderNumber;
let orders = Belt.Array.keep(state.orders, keepOrder);
{…state, orders};
};

let reducer: (action, state) => ReasonReact.update(state, ’b, action) =
(action, state) =>
switch (action) {
| Enter(order) => enterOrder(order, state)->ReasonReact.Update
| ChangeQty(qtyStr) => ReasonReact.Update({…state, qtyStr})
| ChangeSize(sizeStr) => ReasonReact.Update({…state, sizeStr})
| ChangeSleeve(sleeveStr) => ReasonReact.Update({…state, sleeveStr})
| ChangeColor(colorStr) => ReasonReact.Update({…state, colorStr})
| ChangePattern(patternStr) => ReasonReact.Update({…state, patternStr})
| Delete(order) => deleteOrder(order, state)->ReasonReact.Update
};

let makeOrderTable:
(action => unit, array(Shirt.Order.t)) => ReasonReact.reactElement =
(send, orders) => {
let makeOrderItem: Shirt.Order.t => ReasonReact.reactElement =
order => {
let key = string_of_int(order.orderNumber);
let deleteFunction = _event => send(Delete(order));
;
};
let orderItems = Belt.Array.map(orders, makeOrderItem)->ReasonReact.array;

Belt.Array.length(orders) > 0 ?

orderItems

{ReasonReact.string(“Qty”)}

{ReasonReact.string(“Size”)}

{ReasonReact.string(“Sleeve”)}

{ReasonReact.string(“Color”)}

{ReasonReact.string(“Pattern”)}

{ReasonReact.string(“Action”)}

:

{ReasonReact.string(“No orders entered yet.”)}

;
};

let makeActionSend = (send, makeAction, event) =>
ReactEvent.Form.target(event)##valuemakeAction>send;

let render: s => ReasonReact.reactElement =
({state, send}) => {
let {colorStr, errorText, orders, patternStr, qtyStr, sizeStr, sleeveStr} = state;
let qtyLabel = ReasonReact.string(“Qty: ”);
let onChange = makeActionSend(send, actionChangeQty);
let onClick = _event => createOrder(state)actionEnterOrder>send;
let sizeSelect =
makeSelect(
“sizeMenu”,
“Size”,
[|“XS”, “S”, “M”, “L”, “XL”, “XXL”, “XXXL”|],
sizeStr,
makeActionSend(send, actionChangeSize),
);
let sleeveSelect =
makeSelect(
“sleeveMenu”,
“Sleeve”,
[|“Short sleeve”, “Long sleeve”, “Extra-long sleeve”|],
sleeveStr,
makeActionSend(send, actionChangeSleeve),
);
let colorSelect =
makeSelect(
“colorMenu”,
“Color”,
[|“White”, “Blue”, “Red”, “Green”, “Brown”|],
colorStr,
makeActionSend(send, actionChangeColor),
);
let patternSelect =
makeSelect(
“patternMenu”,
“Pattern”,
[|“Solid”, “Pinstripe”, “Checked”|],
patternStr,
makeActionSend(send, actionChangePattern),
);


qtyLabel


sizeSelect
sleeveSelect
colorSelect
patternSelect


{ReasonReact.string(errorText)}

{makeOrderTable(send, orders)}

;
};

let make: array(ReasonReact.reactElement) => t =
_children => {…component, initialState, reducer, render};

2018-12-05Changed name to menuOptionElements, split out the anonymous function to a named function, added and discussed "key" 159TYPO

reason-react/shirt-react/src/OrderForm.re

[|“XS”, “S”, “M”, “L”, “XL”, “XXL”, “XXL”|],

last element should be “XXXL”

2018-12-05Thanks; fixed. 160SUGGEST

“It’s your Turn”

(Modifications to) index.html or Index.re haven’t been discussed yet.

2018-12-05Changes to index.html are on page 161 in the list of things I had to modify. 162TYPO

(1) = = ===
“We need these because the fromString() functions we already have return an option type, and we want the type.”

likely

“We need these because the fromString() functions already return an option type and we want the >value<.”

(2) = = ===

reason-react/shirt-storage/src/Shirt.re

not taking full advantage of “module E = Json.Encode;”, e.g.:

let encodeJson: t => Js.Json.t =
order =>
E.object_([
(“orderNumber”, E.int(order.orderNumber)),
(“quantity”, E.int(order.quantity)),
(“size”, Size.toString(order.size)->E.string),
(“sleeve”, Sleeve.toString(order.sleeve)->E.string),
(“color”, Color.toString(order.color)->E.string),
(“pattern”, Pattern.toString(order.pattern)->E.string),
]);

2018-12-05Fixing the >value< part. Will look at other items later -- also made change to use E instead of Json.Encode. 163SUGGEST

(1) = = ===
reason-react/shirt-storage/src/OrderForm.re

Discussion of encodeState/decodeState has been skipped (perhaps deliberately).

(2) = = ===
“Our code has to create a “neutral” state to return if either of these situations occurs:"

seems like a the old initialState function should be rebranded as “newState” to be used here, e.g.:

let localStorageKey: string = “shirt-orders”;

let storeStateLocally: state => unit =
state =>
encodeState(state)
->Js.Json.stringify
->Dom.Storage.setItem(localStorageKey, _, Dom.Storage.localStorage);

let parseState: string => state =
json =>
switch (Js.Json.parseExn(json)) {
| result => decodeState(result)
| exception _ => newState()
};

let getStoredState: unit => state =
() =>
switch (Dom.Storage.getItem(localStorageKey, Dom.Storage.localStorage)) {
| Some(value) => parseState(value)
| None => newState()
};

let storeState: s => unit = ({state}) => storeStateLocally(state);

2018-12-05Added discussion and code for encodeState/decodeState even though there are no new concepts shown. 164SUGGEST

(1) = = ===
“initialState: () => getStoredState(),”

consider instead

“initialState: getStoredState,”

(2) = = ===

shirt-storage/src/OrderForm.re

in the “Enter(order)” logic consolidate “reset behaviour” by basing the updated state on a “newState()” with the processed fields updated into it, e.g.:

let nextState: (Shirt.Order.t, state) => state =
(order, state) => {
let orders = Belt.Array.concat(state.orders, [|order|]);
let nextOrderNumber = state.nextOrderNumber + 1;
{…newState(), nextOrderNumber, orders};
};

let enterOrder: (Shirt.Order.t, state) => state =
(order, state) => {
let n = intFromStringWithDefault(state.qtyStr, 0);
n > 0 && n <= 100 ?
nextState(order, state) :
{…state, errorText: “Quantity must be between 1 and 100”};
};

let deleteOrder: (Shirt.Order.t, state) => state =
(delete, state) => {
let keepOrder: Shirt.Order.t => bool =
order => order.orderNumber != delete.orderNumber;
let orders = Belt.Array.keep(state.orders, keepOrder);
{…state, orders};
};

let reducer: (action, state) => ReasonReact.update(state, ’b, action) =
(action, state) =>
switch (action) {
| Enter(order) =>
ReasonReact.UpdateWithSideEffects(enterOrder(order, state), storeState)
| ChangeQty(qtyStr) => ReasonReact.Update({…state, qtyStr})
| ChangeSize(sizeStr) => ReasonReact.Update({…state, sizeStr})
| ChangeSleeve(sleeveStr) => ReasonReact.Update({…state, sleeveStr})
| ChangeColor(colorStr) => ReasonReact.Update({…state, colorStr})
| ChangePattern(patternStr) => ReasonReact.Update({…state, patternStr})
| Delete(order) =>
ReasonReact.UpdateWithSideEffects(
deleteOrder(order, state),
storeState,
)
};

2018-12-05Put in first change. 156SUGGEST

reason-react/shirt-react/src/OrderForm.re

Would it make sense to replace the “span” element around the “select” element with a “label” element - and possibly drop the “id” on the “select”?

Similarly for the qty “input” on p.159.

2018-12-05I need the so that the CSS will make it look good (though I did add a

Categories: