'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