Skip to content

Add examples to the Fun module - #12452

Merged
gasche merged 23 commits into
ocaml:trunkfrom
hyphenrf:hazem/fun-examples
Apr 5, 2025
Merged

Add examples to the Fun module#12452
gasche merged 23 commits into
ocaml:trunkfrom
hyphenrf:hazem/fun-examples

Conversation

@hyphenrf

@hyphenrf hyphenrf commented Aug 1, 2023

Copy link
Copy Markdown
Contributor

In a similar spirit to #11476, I tried to add examples of varying complexity to the combinators in Stdlib.Fun.
The convention I chose to follow was the one agreed on in the aforementioned PR.

In addition to examples, I also noted what I think may be "gotchas", namely the evaluation of const's argument and using flip with n > 2 args functions.

Will add a changes entry if this is deemed a worthwhile change. Thanks in advance to kind reviewers!

@hyphenrf hyphenrf mentioned this pull request Aug 1, 2023
@hyphenrf

hyphenrf commented Aug 2, 2023

Copy link
Copy Markdown
Contributor Author

A prototype of these examples was present in the Fun.on PR, which Gabriel gave me some tips on, and I applied them here.

Comment thread stdlib/fun.mli Outdated
Comment thread stdlib/fun.mli Outdated
Comment thread stdlib/fun.mli Outdated
]}
Note that given a function [(f : a -> b -> c -> d)]:
- [(flip f x : a -> c -> d)] and [(x : b)], whereas
- [(flip (f x) : c -> b -> d)] and [(x : a)]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly, I am (as expected) unconvinced by those examples, and I fear that trying to find good examples for flip is a wild good chase. I would propose to split this case to another PR if you want to argue for it in more details.

@hyphenrf hyphenrf Aug 2, 2023

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked up uses in OCaml code out there, and In most cases flip is used

  • to match arg order with another function.. e.g. the List.rev example from here.. which I could replace with a Set.add example at the risk of more code just causing obscurity,
  • to change datalast to datafirst.. e.g. to filter a list by which elements being members of a set: filter (flip S.mem s).. somewhat related to the first case,
  • or to change the arg order of a binary operator where the order has semantic meaning.. e.g. the flip compare and subtract examples.

Comment thread stdlib/fun.mli Outdated
- [(flip f x : a -> c -> d)] and [(x : b)], whereas
- [(flip (f x) : c -> b -> d)] and [(x : a)]

{!val:negate}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might work better to have a thematic predicate section which could contain example of negate, const true and const false.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's value in having one overarching theme in the examples, I agree, but I feel it'll take a bit of power out of the other examples. WDYT? I'll try to come up with nicer examples to the other three following the predicate theme...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could start the negate section a short description of predicates as functions of type 'some -> bool often used in List.find_all and similar functions. Then Fun.negate could be presented as quick (aka without eta-expansion) way to reverse a predicate.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forgot to reply here, I added the passage:

{3:hnegate {{!val:negate}negate}}

Mainly used for reversing a predicate in a function which expects one, like
{!val:List.find_all} and similar functions

I opted not to explain the word "predicate", relying on the precedence in negate's own doc comment, and leaving space for the explanation to be put there instead.

Comment thread stdlib/fun.mli Outdated
[(fun ... -> ...)] explicitly.

The examples below will be demonstrating this mainly with the {!module:List}
module.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering if Seq would not be a better illustration. In particular, we could have a section on building sequences to illustrate const and id and Seq has more sequence constructor like forever or ints to use as a basis of more examples.

@hyphenrf hyphenrf Aug 2, 2023

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem with using Seq, answering also your comment on const above, is in three aspects I imagine:

  • it's less straightforward due to its delay semantics, and its potentially infinite nature.. Instead of focusing on one function, there's a potential to add more distractions (List.of_seq, Seq.take, Seq.equal...)
  • it's less common in code and pedagogical material. list manipulation is one of the first things beginners work through
  • it doesn't play as nicely with the toplevel evaluation theme we have going on, because Seq.t prints as <fun>

@hyphenrf

hyphenrf commented Aug 3, 2023

Copy link
Copy Markdown
Contributor Author

Changes done since the last review:

  • The prologue and the const paragraphs are shortened, more neutral wording in the prologue and equivalence in const removed.
  • flip with n > 2 ary functions is slightly expanded on
  • More examples are added to id and const, particularly the IMO elegant List.filter_map Fun.id replaces List.find_all Fun.id which felt artificial.For const I opted for a somewhat thought provoking example that follows from const id, here used in tandem with fold_left.

@hyphenrf

hyphenrf commented Aug 3, 2023

Copy link
Copy Markdown
Contributor Author
Interesting equivalences:
   - [const id] = [flip const] = [fun _ y -> y]
   - [flip id] = [(|>)] = [fun x f -> f x] (this is because [(|>)] is
     [flip (@@)], and [(@@)] is a specialization of [id : 'a -> 'a] where ['a]
     is unified with [('b -> 'c)])

Thinking about adding this at the bottom. Is it a good idea?

@Octachron Octachron self-assigned this Sep 20, 2023
@NoahTheDuke

Copy link
Copy Markdown

i'd love to see this merged, i think it's good

Comment thread stdlib/fun.mli Outdated
@hyphenrf

hyphenrf commented Feb 7, 2025

Copy link
Copy Markdown
Contributor Author

@NoahTheDuke Thanks for reminding me to resume work on this. I was deliberating on whether or not to split into one PR for each function, but I feel like in doing so we might achieve a good example set in one of them that compromises the rest when the document is considered in full. I'm trying to nail the "overarching theme" as Octachron suggested. I want to avoid local maximums so I'm hoping to finish the discussion here. I'm working on something unrelated currently that's eating up most of my day. When I get a chance in a day or two from now, I should also update the PR to include examples for compose :)

Comment thread stdlib/fun.mli Outdated
@hyphenrf
hyphenrf force-pushed the hazem/fun-examples branch from f2204bf to 3163ee2 Compare March 8, 2025 17:02
@hyphenrf

hyphenrf commented Mar 8, 2025

Copy link
Copy Markdown
Contributor Author

It turned out to be a month not a day or two, apologies!
I've addressed the points raised in reviews and added examples for compose.

The PR should preferably be reviewed as one document at this point,
a) because I admit my commits turned out not so atomic, especially when I noticed I needed to deindent the document or the code blocks turn out indented when seen through make html_doc output, but also
b) because the examples are meant to be readable in one sitting, in the style of the previous referenced PR, so it's valuable to consider the document as a whole, with coherence/continuity as a relation between the examples, not just consider each examples on its own. Some examples reference and build on previous examples even.

I should add here that, at this stage, I'm open to reviewers requesting whole examples and paragraphs removed outright if they deem them redundant or weakening etc. I've added a bit of variety and I'm satisfied with each example. I'm not too attached to any single example as long as the maintainers judge the set they choose to keep meets the desired level of understanding and showcase of utility.

Notably, I've added cautions regarding the value restriction and regarding readability, to guide the reader against going too crazy with these functions.
At the end of that, I wasn't sure how to crossref the manual, and didn't want to reference it by URL until I know if that's an OK thing to do in stdlib docs.

I'd really appreciate a second pair of eyes on the generated document, once the maintainers have approved/selected the content. Suggestions for better prose, grammatical mistakes, and the overall aesthetic with element choices are very welcome. I have written a good amount of documentation, but I'm no ocamldoc guru. There was a best-effort attempt to follow best practices nonetheless.

@hyphenrf

hyphenrf commented Mar 8, 2025

Copy link
Copy Markdown
Contributor Author

CI is complaining about a Changes entry, but do pure doc contributions get an entry? (apparently they do)
Will add a changes entry last.

@hyphenrf

hyphenrf commented Mar 9, 2025

Copy link
Copy Markdown
Contributor Author

(Actually, let me think through compose examples a bit more. I think something with the Format API could be more worthwhile to consider as an example. I'll work more on this PR. I'll also take a second look at the prose in general. I want to shrink the addition a bit and be more focused I think.)

Interestingly for compose, the PR which introduced it (#12770) seems like it was merged without the author making much of a case for the combinator. So I couldn't take a shortcut and use the author's case they would've made in a back-and-forth discussion as an example here.

I personally don't use compose much if at all because it's not an operator, so multiple compositions don't improve readability, and writing out compose in full more often than not turns out to be less concise and more obscure than expanding it. (This is as opposed to an on combinator which this PR was originally a split of, I find myself needing that one from time to time, as it at least removes duplication). You could even try that with the examples I provided, expanding compose makes them more concise and/or more readable.

I did consider an example which shows a pipeline of List.maps, talks about the inefficiency of constructing intermediates like that, then proposes a single map of composed functions as an alternative, but that falls short for n ≠ 2 maps, again because compose is not an operator, and seems more like a thing where converting to Seq instead may be encouraged. I'm very hesitant to define an operator for compose in the docs, because that may implicitly bless it, and everyone has their own operator for it. Maybe in a future where let ( ∘ ) ... is a valid syntax, that will stop being the case, who knows

Perhaps a fun way to use compose is for difference list appending, but I didn't want to introduce that whole concept. It's not a data-structures tutorial. Perhaps if one day in the far future difference lists are included in the stdlib, compose would make a cameo in its docs. 😁

Speaking of the compose examples, these are common use-cases of compose, but perhaps the combinator is useful in something more specific to us? It was a bit hard for me to dig through sherlocode comprehensively for this one, because it's often an operator, and doesn't have "standard" notation (the prior PRs acknowledge that fact). What I did dig out showed pretty much what I included here: selectors like snd, and data transformation. If someone would like to point me to something in the wild where compose has a nice utility, I'd be happy to include it instead of one of or both of the examples. What do you find yourself using compose for?

@hyphenrf
hyphenrf marked this pull request as draft March 9, 2025 07:27
@gasche

gasche commented Mar 9, 2025

Copy link
Copy Markdown
Member

do pure doc contributions get an entry?

Yes, of course!

@hyphenrf

hyphenrf commented Mar 9, 2025

Copy link
Copy Markdown
Contributor Author

Few questions after stepping back and thinking a bit more about the document:

  1. Should the examples be convincing, as in, make you want to use the function? Or is it enough that they showcase how the function may be used, even if they turn out obviously artificial? This could just be shrunk to five simple examples, one for each function, and that be it.
  2. Should the prose be in tutorial style, like it currently is? Or should it be more matter-of-fact and make less suggestions?
  3. Is thematic consistency a worthy goal to pursue?
  4. Should we be limited to REPL-style blocks? Or is pseudo-syntax equations/equivalences style better suited?
  5. Is linking urls (e.g. https://ocaml.org/manual/5.3/polymorphism.html#ss:valuerestriction) in stdlib docs a good idea?
  6. Is it a good idea to introduce data structures like set, map, or difference lists, which would require the user to do more work to reproduce the example?

For me, 1. I'd like strong examples yes, 2. Seems reasonable to guide against future friction, 3. indifferent, 4. It's certainly more flexible, but it's the style used in the doc comments of each function itself. Examples should be the concrete aid to the abstract equation, 5. unsure but depends on 2 anyway, 6. against

Comment thread stdlib/fun.mli Outdated
Comment thread stdlib/fun.mli
let rec chain = function
| [] -> Fun.id
| f :: fs -> fun x -> f (chain fs x)
]}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another advanced example is as a final continuation for functions in continuation-passing-style:

type 'a btree =
  | Leaf of 'a
  | Node of 'a btree * 'a * 'a btree

let rec map_cps f tree k =
  match tree with
  | Leaf v -> k (Leaf (f v))
  | Node (left, v, right) ->
    map_cps f left @@ fun left' ->
    let v' = f v in
    map_cps f right @@ fun right' ->
    Node (left', v', right')

let map f tree = map_cps f tree Fun.id

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was originally in place of chain, but I wasn't sure if introducing the concept of CPS in an example was a case worth defending

Comment thread stdlib/fun.mli Outdated
Comment thread stdlib/fun.mli Outdated
Comment thread stdlib/fun.mli Outdated
of two lists (with order and duplicates from the second)
{[
# List.find_all (Fun.flip List.mem [2; 3; 5]) [0; 3; 3; 2; 4; 6; 8]
- : int list = [3; 3; 2]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really like this example and the one below. They use the form flip f x to partially-apply the second argument of f, which I think is too clever, I would rather write an explicit function in this case.

@hyphenrf hyphenrf Mar 9, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was what I was going for, flip with the second argument partially applied. I don't mind removing it.
The rationale was that it was common enough (you see it in Set, Map, List, Stack, Queue, ... wherever you want to make datafirst become datalast or the opposite)

Update: it is removed

Comment thread stdlib/fun.mli Outdated
Comment thread stdlib/fun.mli Outdated

@gasche gasche left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My overall impression of this category of examples is rather positive. This is a non-trivial, thoughtful contribution, I think it may be of interest to some readers; and it does an appreciated effort of warning about the sins of point-free programming. It may be a good discipline in the future when considering other combinators to ask for similar convincing examples and grow this documentation accordingly.

I propose to wait to see if other people want to give a second read and give another round of feedback. (Ping me in two weeks as I will forget to check.) Then I would propose to go ahead and merge.

@hyphenrf

hyphenrf commented Mar 9, 2025

Copy link
Copy Markdown
Contributor Author

So I couldn't leave this for a while and instead continued work on it haha

I think I'm happy with the example set right now. I chose to shrink some sections to improve the focus in them. I'm ready for reviews and open to example additions (especially a second REPL example in compose... maybe...)

The examples I removed, for reference:

(* id *)
Scanf.scanf "%d" Fun.id

Float.Array.map_from_array Fun.id

if Sys.win32 then String.map (function '/' -> '\\' | c -> c) else Fun.id


(* const *)
let last xs = List.fold_left (Fun.const Option.some) None xs


(* flip *)
let subtract = Fun.flip (-) in
List.map (subtract 2) [4; 6; 8]

List.find_all (Fun.flip List.mem [2; 3; 5]) [0; 3; 3; 2; 4; 6; 8]

(* a flip reference of a present const example *)
let spellcheck known_words =
   String.spellcheck ~max_dist:(Fun.const 2) (Fun.flip List.iter known_words)


(* compose *)
List.find_all (Fun.compose ((=) 3) String.length) ["one"; "two"; "three"]

let buf = Buffer.create 16 in
List.iter (Fun.compose (Buffer.add_utf_8_uchar buf) Uchar.of_int) [0x49; 0x2764; 0xfe0f; 0x1f42b];
Buffer.contents buf

@hyphenrf
hyphenrf marked this pull request as ready for review March 9, 2025 16:40
@gasche

gasche commented Mar 31, 2025

Copy link
Copy Markdown
Member

@hyphenrf I would like to move to merge this, but for this we need two things:

  • there should be a Changes entry; documentation contributions are also important
  • our check-typo script complains that fun.mli line 226 has 81 columns, you need to break lines to fit in 80 columns.

@hyphenrf
hyphenrf force-pushed the hazem/fun-examples branch from 9575b93 to a2516f9 Compare April 1, 2025 12:49
@hyphenrf

hyphenrf commented Apr 1, 2025

Copy link
Copy Markdown
Contributor Author

Done (Also updated the branch because I had some local issues with the shallow repo I cloned and originally based this branch on, sorry for the noise).

I'm struggling with rendering the docs in a modern format (the make target I invoke ends up creating old-style docs) and I just need confirmation the style is consistent and follows convention by anyone who knows their way around the doc generation pipeline better...

For reference, this is exactly what I did to generate the docs:

./configure
make core
make html_doc
xdg-open ./api_docgen/ocamldoc/build/html/libref/Fun.html

image

@gasche
gasche merged commit 0ef1210 into ocaml:trunk Apr 5, 2025
@gasche

gasche commented Apr 5, 2025

Copy link
Copy Markdown
Member

Merged! We haven't had a final look by documentation-building experts, but I'm impatient to get the nice contribution in so I merged anyway. If there are minor rendering glitches we can always fix them later.

@hyphenrf

hyphenrf commented Apr 5, 2025

Copy link
Copy Markdown
Contributor Author

Well, I'm thankful for the patience with the reviews 😁
Thanks all

@hyphenrf
hyphenrf deleted the hazem/fun-examples branch April 5, 2025 10:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants