By Developers, For Developers
PDF Pg | Paper Pg | Type | Description | Fixed on | Comments | ||||||
---|---|---|---|---|---|---|---|---|---|---|---|
40 | TYPO | ‘We’d like to use Belt.Option.map() to eliminate the switch in line 12’ | 2018-11-26 | Thanks. I have changed the wording to mention the switch but focus on toFixedWithPrecision, which is the important part. | |||||||
47 | SUGGEST | 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? Also to avoid installing parcel in a project (or globally) one could use npx (npx parcel src/index.html). | 2018-11-26 | The server with localhost:1234 is mentioned in a sidebar on page 48. | |||||||
63 | TYPO | 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-26 | This 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. | |||||||
75 | SUGGEST | Last case in switch inside ‘toOrder’ function seems a bit confusing: 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-26 | Fixed, thanks. | |||||||
76 | TYPO | ‘The result of Js_string.match()’ should be ‘Js.String.match()’ | 2018-11-26 | Although 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. | |||||||
1 | SUGGEST | 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-07 | Thanks. 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. | |||||||
46 | ERROR | let calcButton = Doc.getElementById(“calculate”, Dom.document); should say let calcButton = Doc.getElementById(“calculate”, D.document); | 2018-11-26 | Thanks. Fixed. | |||||||
54 | SUGGEST | Chapter 4 is quite confusing. Looking forward for more updates! | 2018-11-27 | 1) 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. | |||||||
3 | TYPO | 4th bullet point. “ReasonML is requires…”. Remove the word “is”. | 2018-11-26 | Thanks; will be fixed in next beta. | |||||||
7 | SUGGEST | “The naming convention for ReasonML follows the JavaScript convention | 2018-11-26 | Good idea! | |||||||
119 | TYPO | Last sentence in ‘Handling Missing Fields’ hint/sidebar: Should probably be ‘Js.Nullable.isNullable()’ | 2018-11-27 | Thanks; will fix. | |||||||
46 | TYPO | Webapi.Dom.EventTarget.addEventListener should be D.EventTarget.addEventListener for consistency. | 2018-11-26 | Will fix; thanks. | |||||||
26 | TYPO | 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; Js.log(“Loan of 10000 at 5%”); | 2018-11-26 | Fixing the incorrect number of decimals. | |||||||
24 | SUGGEST | An interesting variation may be: let callGermany = call(“049”); | 2018-12-05 | I might add that in the code as a comment. | |||||||
21 | SUGGEST | 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 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 (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-26 | Interesting -- 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. | |||||||
21 | TYPO | 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-05 | This 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. | |||||||
7 | SUGGEST | 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-26 | I'll add "lower camel case". | |||||||
33 | TYPO | datatypes/src/ParamShirtSizes.re Js.log(stringOfShirtSize(veryBigSize)); /* output: M */ should be Js.log(stringOfShirtSize(veryBigSize)); /* output: XXXL */ | 2018-11-27 | Will fix; thanks. | |||||||
33 | SUGGEST | 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-07 | Leaving as is. | |||||||
41 | SUGGEST | You have set up the perfect opportunity to introduce (fast) pipes - especially before the “It’s Your Turn” exercise. let makeDisplayText: option(string) => string = let method2: string => unit = 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) = let myFlatMap: (option(’a), ’a => option(’b)) => option(’b) = | 2018-11-26 | I 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) | |||||||
40 | SUGGEST | “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-26 | I think I have a way to work this in... | |||||||
40 | TYPO | “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-26 | Good catch - will fix. | |||||||
39 | TYPO | “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-27 | Will fix; thanks. | |||||||
37 | SUGGEST | For datatypes/src/OptionShirtSizes.re please consider the following variation: let toFixed = Js.Float.toFixedWithPrecision; let makeDisplayText: (string, float) => string = let displayPrice: string => unit = 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-26 | That sounds good. | |||||||
35 | SUGGEST | 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 much more satisfying and to the point. | 2018-11-26 | I prefer open-ended exercises, but having a specification of the intended output is probably good here. | |||||||
52 | SUGGEST | let _ = 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 _ = 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; In fact refmt doesn’t modify the second version while it rewrites the first as: let _ = 1) not explicitly marking the piped value destination (because it wasn’t marked in the first place) | 2018-11-26 | Will add further explanation. | |||||||
51 | SUGGEST | “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); 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-26 | I 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. | |||||||
51 | SUGGEST | … continued, example: /* Convert fractional monetary unit (cents) to basic monetary unit (dollars) */ let priceString: string = | 2018-11-26 | ||||||||
47 | SUGGEST | This simple setup seems to work reasonably well: package.json: { Terminal session: webpage$ npm run scratch > shirts@0.1.0 scratch …/reason/web_dev/webpage > shirts@0.1.0 clean …/reason/web_dev/webpage > shirts@0.1.0 clean:dist …/reason/web_dev/webpage > shirts@0.1.0 clean:bsb …/reason/web_dev/webpage Cleaning… 410 files. > shirts@0.1.0 build …/reason/web_dev/webpage [164/164] Building src/core/Base64Re.mlast.d Server running at localhost:1234 webpage$ jobs 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 >>>> Start compiling BuckleScript Github Issue #2750 "bsb -make-world | 2018-12-05 | Leaving things as they are. | |||||||
63 | SUGGEST | 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 = with a reference to the section discussing exception handling. | 2018-12-04 | Adding some verbiage to show this other way of handling errors. | |||||||
65 | SUGGEST | 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.”
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 = 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-05 | Added 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 | |||||||
65 | SUGGEST | “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 val keepU : ’a t -> (’a -> bool [ 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-26 | The 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) | |||||||
66 | SUGGEST | let addPrice = (accumulator, orderItem) => accumulator +. orderPrice(orderItem); Instead of “accumulator” wouldn’t something like “currentTotal”, “runningTotal”, or “tally” be clearer? 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 addPrice = (currentTotal, order) => currentTotal + priceOrder(order); let totalPrice = Belt.List.reduce(orders, 0, addPriceLogged); 0 8750 | 2018-11-26 | I am making the name change; not sure about adding the Js.log stuff. Let me give that some thought. | |||||||
68 | SUGGEST | collections/src/MapKeepReduce.re readability let mediumTotal = 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-27 | Keeping this as is, because I am presuming most of the readers will be the uninitiated. | |||||||
70 | SUGGEST | 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 = Js.log(stringOfList2(intValues, string_of_int)); /* [10, 11, 12, 13, 14, 15] */ But then again maybe you are deliberately avoiding OCaml’s standard library in favour of BuckleScript centric libraries like Belt and Js. | 2018-11-27 | Yes, I am staying with Belt as much as possible, then Js. | |||||||
73 | TYPO | collections-app/src/index.html Shirt Price Calcuator should be Shirt Price Calculator | 2018-11-26 | Fixed; thanks. | |||||||
75 | SUGGEST | 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) = Also let commaSplit2: string => array(string) = seems more straightforward though I imagine this is meant as a warmup to using regular expressions. | 2018-12-04 | Added verbiage about {|...|} but am leaving it as a regex. | |||||||
75 | SUGGEST | collections-app/src/OrderPage.re readability type order = (int, ShirtSize.t); let makeOrderError: string => t = input => Belt.Result.Error(input); let makeOrderResult: (option(ShirtSize.t), array(string), string) => t = let orderOfCaptures: (option(array(string)), string) => t = let toOrder: string => t = let orderPrice: order => int = ((n, size)) => n * ShirtSize.price(size); let addOrderTotal: ((int, int), t) => (int, int) = let aggregateOrderTotals: array(t) => (int, int) = let toBasic: int => float = fractional => float_of_int(fractional) /. 100.0; let makeOrderRow: order => string = | |||||||||
$n |
$shirtSize |
\\$$totalPrice |
|||||||||
Bad input $input |
|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 => ()
};
Variable name “optOrder” - value is actually a Belt.Result.t, not a Belt.Option.t.
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.
“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.
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));
“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;
recursion/keep-indices/src/WithoutHelper.re
FYI: refmt will transform the “switch on boolean expression” to the ternary operator.
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);
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));
“You’ll this in action as we create custom modules that serve as arguments to other modules.”
Possibly
“You’ll see this …”
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 */
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”)
“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.
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);
};
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]);
};
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);
};
“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”
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);
};
“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).
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);
“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?")
}
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 */
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, ());
“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.
“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.
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);
“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), “”,
“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.
“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
“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?
“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?
(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
“Once you have your components, you tell React to render them into a web pag”. pag => page
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?
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.
“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”.
“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”
);
(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.”
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
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>
Notices
reason-react/notices/src/Index.re
ReactDOMRe.renderToElementWithId(
,
“root”,
);
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};
};
“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).
(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 ?
{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};
reason-react/shirt-react/src/OrderForm.re
[|“XS”, “S”, “M”, “L”, “XL”, “XXL”, “XXL”|],
last element should be “XXXL”
“It’s your Turn”
(Modifications to) index.html or Index.re haven’t been discussed yet.
(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),
]);
(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);
(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,
)
};
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.
“It’s Your Turn”
At this point I felt that “OrderForm.re” already had gotten rather monolithic which is why I skipped this exercise and proceeded to the end of the chapter without adding the new features. Then I doubled back and I had a closer look at the exercise again and realized that there was a perfect exercise BEFORE the described one. Refactoring the existing solution to some common React Patterns, in particular:
The starting point can be simply outlined with some simple components:
/* file: src/Index.re
References:
medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0
reactjs.org/docs/lifting-state-up.html
github.com/reasonml/reason-react/blob/master/docs/component-as-prop.md
*/
/* Functions to wire presentational components to container component
These renderProps also contain functionality typically provided by
mapStateToProps / mapDispatchToProps in Redux
as they adapt the container’s state and dispatch
to the presentation components props.
*/
let withEdit: string => ReasonReact.reactElement =
viewName =>
let withError: string => ReasonReact.reactElement =
error =>
let withView: string => ReasonReact.reactElement =
viewName =>
/* “Render” the container component passing the presentational components
through renderProps. The container component is the top level component
“lifting the state up”.
*/
ReactDOMRe.renderToElementWithId(
“root”,
);
/* file: src/Orders.re */
module Rr = ReasonReact;
type state = {count: int};
type action =
| Go;
let component = Rr.reducerComponent(“Orders”);
let initialState = () => {count: 0};
let reducer = (action: action, state) =>
switch (action) {
| _ => Rr.Update({count: state.count + 1})
};
let make = (~withEdit, ~withError, ~withView, _children) => {
/* Rendering is delegated to the components that are passed in via renderProps */
let render = _self =>
Rr.array([|
withEdit(“editView”),
withError(“errorView”),
withView(“ordersView”),
|]);
{…component, initialState, reducer, render};
};
/* file: src/ErrorView.re */
module Rr = ReasonReact;
type retainedProps = {error: option(string)};
type t =
Rr.componentSpec(
Rr.stateless,
Rr.stateless,
retainedProps, /* passed to the lifecycle methods */
retainedProps, /* in the component record from “make” BUT NOT “component” */
Rr.actionless,
);
/* lcea: lifecycle event argument
See also: jaredforsyth.com/reason-react/api/ReasonReact.html
reasonml.github.io/reason-react/docs/en/lifecycles
*/
type lcea =
ReasonReact.oldNewSelf(
ReasonReact.stateless,
retainedProps,
ReasonReact.actionless,
);
let component = ReasonReact.statelessComponentWithRetainedProps(“ErrorView”);
/* reasonml.github.io/docs/en/function#optional-labeled-arguments */
let make: (~error: string=?, array(Rr.reactElement)) => t =
(~error=?, _children) => {
let retainedProps = {error: error};
/* Only need to render if a DIFFERENT error message (including NO error message) has been issued */
let shouldUpdate: lcea => bool =
({oldSelf, newSelf}) =>
oldSelf.retainedProps.error != newSelf.retainedProps.error;
let render = _self =>
switch (error) {
| Some(message) =>
{ReasonReact.string(message)}
| None =>
};
{…component, retainedProps, shouldUpdate, render};
};
/* file: src/OrderEditView.re */
module Rr = ReasonReact;
let component = Rr.statelessComponent(“OrderEditView”);
let make = (~viewName: string, _children) => {
let render = _self =>
{Rr.string(viewName)}
;
{…component, render};
};
/* file: src/OrdersView.re */
module Rr = ReasonReact;
let component = Rr.statelessComponent(“OrdersView”);
let make = (~viewName: string, _children) => {
let render = _self =>
{Rr.string(viewName)}
;
{…component, render};
};
In the paragraph just under “Getting a Value”, in the last sentence, there is a text in parentheses:
(In terms used by object-oriented programming languages, you could say we are down-casting an Element to the HtmlELement subclass.)
I believe it should be HtmlElement (with lower case l).
In the first expression of the code sample it is two levels of switch-bodies, but only one switch, either the last “| None => None; };” has to be removed or an addition switch has to be added for the example to be more correct.
I’m getting a type error from the commaSplit function used in OrderPage.re. It happens with the code snippet copied from the book and the sample code from Github (which seems identical).
let commaSplit = (s: string) : array(string) => {
let pattern = [%re “/\\\\s*,\\\\s*/”];
Js.String.splitByRe(pattern, s) ->
Belt.Array.map(_, (item) => {
Belt.Option.getWithDefault(item, “”)
})
};
We’ve found a bug for you!
11 │ let commaSplit = (s: string) : array(string) => {
12 │ let pattern = [%re “/\\\\s*,\\\\s*/”];
13 │ Js.String.splitByRe(pattern, s) ->
14 │ Belt.Array.map(_, (item) => {
15 │ Belt.Option.getWithDefault(item, “”)
This has type:
array(Js.String.t)
But somewhere wanted:
array(option(string))
The incompatible parts:
Js.String.t (defined as string)
vs
option(string)
Near bottom of page 3,
Should be:
you@computer:~/book-projects> cd first-project
you@computer:~/book-projects/first-project> npm run build
not
you@computer:~/book_projects> cd first_project
you@computer:~/book_projects/first_project> npm run build
In Setting Up the Project,
before:
> npm install —save bs-webapi
Add:
> cd shirts
Otherwise it won’t build.
When using WebStorm as an IDE, WebShirts.re line 1:
module D = Webapi.Dom
has Webapi.Dom with error squiggles, and error message:
“The module or file Webapi can’t be found…”
You can ignore the warning. When bundled with parcel, it will build and run.
When I run bsrefmt on
let avg = (a, b) => {
(a +. b) /. 2.0;
};
it doesn’t change it at all! The book text says it should output:
let avg = (a, b) => (a +. b) /. 2.0;
But for me, the before and after are the same.
Following up from my previous errata for this page, I find that refmt DOES produce the output that the book specifies for bsrefmt.
“refmt —version” gives: “Reason 3.3.3 @ fefe5e4d”
“bsrefmt —version” gives: “Reason 3.4.2 @ 3c6a9ca9”
The line “THus, int_of_float(14.50) returns” should be “Thus, int_of_float(14.50) returns”, the “h” should be lower case.
In
```
first-project/src/ConcatDiscount.re
Js.log(“Price before discount: $” string_of_float(total) “.”); Js.log(“Price after discount: $” string_of_float(afterDiscount) “.”);
```
`Pervasives.string_of_float` is deprecated and the compiler now suggests `Js.Float.toString`
Hey there,
This is a small suggestion I came up with when finishing the chapter on collections and trying to do the final exercise. Since you break it up into smaller pieces (there’s the challenge, there’s the variant of using min and max instead of our own functions, there’s the bonus part of finding the index and then the extra-bonus of creating the webpage), I thought it would be a good idea to provide your solution accordingly, instead of having one file just for the latter (the extra-bonus).
Personally, I am learning reasonML and approaching functional programming for the first time, so I appreciate splitting up the challenges into smaller pieces. However, I am left with no solution for me to easily compare mine to. The file you provide is just “too big and too scary” as it doesn’t address the mini-challenges I’m taking on. Instead, what I would do is to provide a file with a solution for each of the steps so the reader can compare it as he goes.
Thanks
When you have this paragraph:
Getting the Quantity and Unit Price
Using the shirtSizeOfString() function that we developed on page 38, we have all
the tools we need to get the information from the quantity and size fields:
You mention that you are using the shirtSizeofString function, but it actually needs the shirtSize type and also the price function as well.
I think including that would be an welcome addition, something like
Using the type shirtSize and shirtSizeOfString and price function we developed in page N.
:)
When interoping with JS through the date module, the compiler will throw warnings about unnamed functions. The code currently looks like this:
type t;
[bs.new] external createDate: unit => t = "Date";
[
bs.scope “Date”] [bs.val] external now: unit => float = "";
[
bs.scope “Date”] [bs.val] external jsDateParse: string => float = "parse";
[
bs.send] external toString: t => string = “”;
[@bs.send] external getFullYear: t => float = “”;
While what the compiler really wants is this:
type t;
[bs.new] external createDate: unit => t = "Date";
[
bs.scope “Date”] [bs.val] external now: unit => float = "now";
[
bs.scope “Date”] [bs.val] external jsDateParse: string => float = "parse";
[
bs.send] external toString: t => string = “toString”;
[@bs.send] external getFullYear: t => float = “getFullYear”;
This code, together with an explanation of why the second name is wanted, would be really nice.
As far as I can tell, where you say: “The underscore in the setInnerText() call skips the first positional parameter and partially applies the priceString” you are relying on a feature that might be removed. To see what I mean go to the URL reasonml.chat/t/partial-application-and/714/3
The code example at the top of the page doesn’t work in /reasonml.github.io/en/try.html. It gets an error: “This variant constructor, Some, expects 1 argument; here, we’ve found 2.” I don’t immediately see how to fix it.
In the list of steps on this page, the use of addPrice() is discussed. However that function is not defined until the following page. Rather, addPriceLogged() has been defined at that point.
The text on this page mentions “algebraic data types” but that is the ONLY time the word “algebraic” occurs in the entire book, so the reader cannot be expected to have any idea what it means. Some explanation would appear to be in order.
Type t is defined:
type t(’a, ’b) =
| Ok(a)
| Error(b);
But that does not compile in reasonml.github.io/en/try.html. Do you mean
type t(’a, ’b) =
| Ok(’a)
| Error(’b);
The two Js.String.splitByRe examples at the bottom of the page each have an extra right paren, causing them not to compile.
In the toInt function, exception(Failure(“int_of_string”)) is used but this may change in the future so the compiler gives a warning. From the OCaml docs:
val int_of_string : string -> int
Same as int_of_string_opt, but raise Failure “int_of_string” instead of returning None.
exception Failure of string
Exception raised by library functions to signal that they are undefined on the given arguments. The string is meant to give some information to the programmer; you must not pattern match on the string literal because it may change in future versions (use Failure _ instead).
So, instead of using the string literal, I would suggest using _. Especially since the type of exception doesn’t matter — we would want to catch all types.
Page 95 mentions “You can see my solution at the end of file code/recursion/palindrome/src/Palindrome.re”. However, this code has a bug where if the string “ab” is entered into isPalindrome2, it will incorrectly return true.