'How to use FSharpPlus.Lens to specify the index of a list?
The sample code of the documentation defines _pageNumber
using List._item
, but I can't seem to find an example of its use.
I tried the following code but it gave an error.
view (Book._pageNumber 1) rayuela // error
How would it be used?
Solution 1:[1]
Brian's answer is very accurate from the technical viewpoint but conceptually misses the most important point: you're "viewing" a partial lens (also called prism), instead of "previewing" it. This is not a limitation of F#+, this is just how lens behaves.
Some background: Prisms or partial lenses are like a lens that can fail, so in principle you can't use the view
operation on them because it's an operation that always succeed, or better said doesn't consider a failure, you should use the preview
operation which returns an option.
The composition rules state that the result of composing:
- a lens with a lens is a lens
- a lens with a prism (or the other way around) is a prism
- a prism with a prism is a prism
This is, as soon as there is a prism in the composition chain the result will be a prism.
In our case we have _pages << List._item i << _Some
which are lens composed with lens composed with _Some
which is a prism, so _pageNumber i
will be a prism.
Now, what happens if you use view for a prism? The zero
value represents a failure, for instance the zero value of an option is None, but here there is no zero value specified.
Brian is right in that the error message is misleading, a better error would be "don't use view over a prism", but instead what happen is to try to get a naked value (not inside an option) which can represent failures with zero
.
TL; DR
use instead:
preview (Book._pageNumber 1) rayuela // Some { Contents = "The End" }
Someone should send a PR to add that line to the docs.
Solution 2:[2]
I'm seeing the same thing:
No overloads match for method 'Zero'".
The problem is caused by the _Some
lens, which doesn't work with record types, because they don't have a default (i.e. "zero") value:
let inline _pageNumberOpt i b =
_pages << List._item i <| b
let pageOpt = view (_pageNumberOpt 1) rayuela // this is fine
let page = view _Some pageOpt // this doesn't work, because the input is an Option<Page>
let x = view _Some (Some 1) // this works, because the input is an Option<int>
This appears to be a limitation in FSharpPlus that wasn't accounted for in the documentation. If you want to work around the problem, you can define Page.Zero
yourself, and then the example will compile:
type Page =
{ Contents: string }
static member Zero = { Contents = "" }
let page = view (Book._pageNumber 1) rayuela
printfn $"{page}" // output is: { Contents = "The End" }
let noPage = view (Book._pageNumber 5) rayuela
printfn $"{noPage}" // output is: { Contents = "" }
Page.Zero
will only be called if you ask for a page that doesn't exist, but it needs to be there for the compiler in any case.
(FWIW, in my experience, FSharpPlus is a very, very delicate beast. It's an interesting experiment, but it breaks easily. And when it breaks, the compiler errors are mind-boggling.)
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 |