diff --git a/lens/private/base/laws.scrbl b/lens/private/base/laws.scrbl index 8ff6073..352ec01 100644 --- a/lens/private/base/laws.scrbl +++ b/lens/private/base/laws.scrbl @@ -2,7 +2,7 @@ @(require "../doc-util/main.rkt") -@title{Lens Laws} +@title[#:tag "laws"]{Lens Laws} While @racket[make-lens] allows lenses to be constructed from arbitrary getters and setters, these getters and setters diff --git a/lens/private/dict.scrbl b/lens/private/dict.scrbl index f6170c4..57f020a 100644 --- a/lens/private/dict.scrbl +++ b/lens/private/dict.scrbl @@ -3,7 +3,9 @@ @(require "doc-util/main.rkt") -@title{Dict lenses} +@title[#:tag "dict-reference"]{Dict lenses} + +@see-guide-note["dict-guide"]{dictionary lenses} @defproc[(dict-ref-lens [key any/c]) lens?]{ Returns a lens for viewing the value mapped to @racket[key] in a dict. diff --git a/lens/private/doc-util/main.rkt b/lens/private/doc-util/main.rkt index 5f265b2..fd7b347 100644 --- a/lens/private/doc-util/main.rkt +++ b/lens/private/doc-util/main.rkt @@ -16,6 +16,7 @@ for-label racket/stream racket/set racket/contract + racket/function for-syntax racket/base syntax/parse diff --git a/lens/private/doc-util/other-reference.rkt b/lens/private/doc-util/other-reference.rkt index fe7e20b..c5540ab 100644 --- a/lens/private/doc-util/other-reference.rkt +++ b/lens/private/doc-util/other-reference.rkt @@ -5,7 +5,7 @@ (only-in scribble/core style) setup/collects) -(provide other-reference-note) +(provide other-reference-note see-guide-note see-reference-note) (define css-resource (make-css-addition @@ -23,3 +23,11 @@ (define (other-reference-note . pre-content) (margin-note (flexible-container finger (apply flexible-element pre-content)))) + +(define (see-guide-note tag . pre-content) + @other-reference-note{ + @seclink[tag]{The Lens Guide} has additional examples of @|pre-content|.}) + +(define (see-reference-note tag . pre-content) + @other-reference-note{ + @seclink[tag]{The Lens Reference} has additional information on @|pre-content|.}) diff --git a/lens/private/hash/main.scrbl b/lens/private/hash/main.scrbl index 44f3a08..b0c466e 100644 --- a/lens/private/hash/main.scrbl +++ b/lens/private/hash/main.scrbl @@ -1,8 +1,11 @@ #lang scribble/manual -@(require "../doc-util/scribble-include-no-subsection.rkt") +@(require "../doc-util/main.rkt" + "../doc-util/scribble-include-no-subsection.rkt") -@title{Hash Lenses} +@title[#:tag "hash-reference"]{Hash Lenses} + +@see-guide-note["hash-guide"]{hash lenses} @scribble-include/no-subsection["ref.scrbl"] @scribble-include/no-subsection["nested.scrbl"] diff --git a/lens/private/list/main.scrbl b/lens/private/list/main.scrbl index 6a41280..48a84a6 100644 --- a/lens/private/list/main.scrbl +++ b/lens/private/list/main.scrbl @@ -1,6 +1,10 @@ #lang scribble/manual -@title{Pair and List Lenses} +@(require "../doc-util/main.rkt") + +@title[#:tag "pair-list-reference"]{Pair and List Lenses} + +@see-guide-note["pair-list-guide"]{pair and list lenses} @include-section["car-cdr.scrbl"] @include-section["list-ref-take-drop.scrbl"] diff --git a/lens/private/scribblings/guide.scrbl b/lens/private/scribblings/guide.scrbl index 97a49a7..4f08cd0 100644 --- a/lens/private/scribblings/guide.scrbl +++ b/lens/private/scribblings/guide.scrbl @@ -10,3 +10,4 @@ features this library provides; for a complete API reference, see @secref{lens-r @local-table-of-contents[] @include-section["guide/introduction.scrbl"] +@include-section["guide/built-in.scrbl"] diff --git a/lens/private/scribblings/guide/built-in.scrbl b/lens/private/scribblings/guide/built-in.scrbl index 054e540..e5bd372 100644 --- a/lens/private/scribblings/guide/built-in.scrbl +++ b/lens/private/scribblings/guide/built-in.scrbl @@ -9,6 +9,14 @@ @(define-syntax-rule (lens-interaction expr ...) (interaction #:eval (make-lens-eval) expr ...)) -@title[#:tag "builtin-lenses"]{Lenses on Built-In Datatypes} +@title[#:tag "builtin-lenses" #:style 'toc]{Lenses on Built-In Datatypes} +This library provides @lens-tech{lenses} for most built-in Racket datatypes. In general the name of +each lens corresponds to the name of its accessor function, with @racket[-lens] appended to the end. +For example, the lens for accessing the first element of a pair is @racket[car-lens], and the lens for +accessing an element of a hash is called @racket[hash-ref-lens]. +@local-table-of-contents[] + +@include-section["built-in/ordered.scrbl"] +@include-section["built-in/key-value.scrbl"] diff --git a/lens/private/scribblings/guide/built-in/key-value.scrbl b/lens/private/scribblings/guide/built-in/key-value.scrbl new file mode 100644 index 0000000..3d4118f --- /dev/null +++ b/lens/private/scribblings/guide/built-in/key-value.scrbl @@ -0,0 +1,90 @@ +#lang scribble/manual + +@(require scribble/eval + "../../../doc-util/main.rkt") + +@(define make-lens-eval + (make-eval-factory '(racket/base lens))) + +@(define-syntax-rule (lens-interaction expr ...) + (interaction #:eval (make-lens-eval) expr ...)) + +@title[#:tag "key-value-lenses"]{Lenses on Key-Value Data} + +Many Racket data structures hold values that correspond to a given key. Lenses for accessing elements +of these structures by their keys are provided. + +@section[#:tag "hash-guide"]{Hash Tables} + +@see-reference-note["hash-reference"]{hash lenses} + +Racket hash tables are simple key-value associations, and as a result, they only have one primitive +lens constructor, @racket[hash-ref-lens]. Given a key, it produces a lens which views the value +associated with the lens: + +@(lens-interaction + (lens-transform (hash-ref-lens 'a) (hash 'a "Hello") + (λ (s) (string-append s ", world!")))) + +Note that @racket[hash-ref-lens]'s signature differs from that of @racket[hash-ref] in an important +way: it does not accept a "failure result" if the key is missing from the hash. Instead, the lens +always throws an error: + +@(lens-interaction + (lens-view (hash-ref-lens 'not-a-key) (hash))) + +This may seem inconvenient, but this limitation is by design---supporting a failure result would +violate one of the @seclink["laws"]{lens laws}. Specifically, “get-set consistency” would no longer +hold. Consider this example: + +@(racketblock + (let ([l (hash-ref-lens 'not-a-key "default")] + [h (hash)]) + (lens-set l h (lens-view l h)))) + +If @racket[hash-ref-lens] accepted a default value, then the above expression would produce a new hash +that was not @racket[equal?] to the original target. Enforcing this property makes lenses easier to +reason about, just as ensuring purity makes functions easier to reason about. + +Of course, sometimes breaking purity is the easiest way to solve a problem, and similarly, sometimes +breaking the lens laws is okay (though it should be avoided if possible). We could, if we wished, +define our own hash lens that accepts a default value: + +@(define ref-default-eval (make-lens-eval)) +@(interaction #:eval ref-default-eval + (define (hash-ref-lens/default key failure-result) + (make-lens (λ (h) (hash-ref h key failure-result)) + (λ (h v) (hash-set h key v))))) + +With this custom, "naughty" lens, we can actually perform the example from above: + +@(interaction #:eval ref-default-eval + (let ([l (hash-ref-lens/default 'not-a-key "default")] + [h (hash)]) + (lens-set l h (lens-view l h)))) + +In addition to @racket[hash-ref-lens], @racket[hash-ref-nested-lens] is provided, which assists in +fetching values from nested hashes. It is defined in terms of @racket[hash-ref-lens] and +@racket[lens-compose], so it is just a shorter way of expressing the same concept: + +@(lens-interaction + (lens-set (hash-ref-nested-lens 'a 'b 'c) + (hash 'a (hash 'b (hash 'c "foo"))) + "bar")) + +@section[#:tag "dict-guide"]{Dictionaries} + +@see-reference-note["dict-reference"]{dictionary lenses} + +Racket @tech[#:doc '(lib "scribblings/reference/reference.scrbl")]{dictionaries} provide a generic +interface for many kinds of key-value data-structures. They encompass hash tables, association lists, +user-defined dictionaries, and even integer-keyed structures like vectors. + +In practice, dictionary lenses work identically to lenses on hashes. The @racket[dict-ref-lens] +lens constructor creates a lens with a view that is the value associated with the lens's key. + +@(lens-interaction + (lens-transform (dict-ref-lens 'b) + '((a . 1) + (b . 2)) + (λ (x) (* x 2)))) diff --git a/lens/private/scribblings/guide/built-in/ordered.scrbl b/lens/private/scribblings/guide/built-in/ordered.scrbl new file mode 100644 index 0000000..a83a14a --- /dev/null +++ b/lens/private/scribblings/guide/built-in/ordered.scrbl @@ -0,0 +1,107 @@ +#lang scribble/manual + +@(require scribble/eval + "../../../doc-util/main.rkt") + +@(define make-lens-eval + (make-eval-factory '(racket/base racket/function racket/list racket/stream lens))) + +@(define-syntax-rule (lens-interaction expr ...) + (interaction #:eval (make-lens-eval) expr ...)) + +@title[#:tag "ordered-data-lenses"]{Lenses on Ordered Data} + +Many Racket data structures hold @emph{ordered} or @emph{sequential} values. Lenses for accessing +elements of these structures by index are provided. + +@section[#:tag "pair-list-guide"]{Pairs and Lists} + +@see-reference-note["pair-list-reference"]{pair and list lenses} + +The two primitive pair lenses are @racket[car-lens] and @racket[cdr-lens]: + +@(lens-interaction + (lens-transform car-lens '(1 . 2) (curry * 2)) + (lens-transform cdr-lens '(1 . 2) (curry * 2))) + +Obviously, these also work with lists, but most of the time, it's easier to use list-specific lenses. +For arbitrary access to elements within a list, use the @racket[list-ref-lens] lens constructor, which +produces a new lens given an index to look up. Abbreviation lenses such as @racket[first-lens] and +@racket[second-lens] are provided for common use-cases: + +@(lens-interaction + (lens-transform (list-ref-lens 3) (range 10) sub1) + (lens-transform third-lens (range 10) sub1)) + +Using @racket[list-ref-lens], it is possible to create a lens that performs indexed lookups for nested +lists: + +@(lens-interaction + (define (2d-list-ref-lens x y) + (lens-compose (list-ref-lens x) + (list-ref-lens y))) + (lens-set (2d-list-ref-lens 1 2) + '((1 2 3) + (4 5 6) + (7 8 9)) + 0)) + +This can also be generalized to @emph{n}-dimensional lists: + +@(lens-interaction + (define (list-ref-lens* . indicies) + (apply lens-compose (map list-ref-lens indicies))) + (lens-set (list-ref-lens* 0 1 0) + '(((a b) (c d)) + ((e f) (g h))) + 'z)) + +This function is actually provided by @racketmodname[lens] under the name +@racket[list-ref-nested-lens], but it's easy to implement yourself. + +@subsection{Fetching multiple list values at once} + +Sometimes it can be useful to fetch multiple values from a list with a single lens. This can be done +with @racket[lens-join/list], which combines multiple lenses whose target is a single value and +produces a new lens whose view is all of those values. + +@(lens-interaction + (define first-two-lens (lens-join/list first-lens second-lens)) + (lens-view first-two-lens '(1 2 3 4)) + (lens-set first-two-lens '(1 2 3 4) '(a b)) + (lens-transform first-two-lens '(1 2 3 4) (curry map sub1))) + +This can be useful to implement a form of information hiding, in which only a portion of a list is +provided to client code, but the result can still be used to update the original list. + +@section[#:tag "vectors-strings-guide"]{Vectors and Strings} + +@other-reference-note{ + The @secref["vectors-reference"] and @secref["strings-reference"] sections in The Lens Reference + have additional information on vector and string lenses, respectively.} + +Lenses for random-access retrieval and functional update on vectors and strings are similar to the +lenses provided for lists, but unlike lists, they are truly random-access. The +@racket[vector-ref-lens] and @racket[string-ref-lens] lens constructors produce random-access lenses, +and @racket[lens-join/vector] and @racket[lens-join/string] combine multiple lenses with vector or +string targets. + +@(lens-interaction + (lens-transform (vector-ref-lens 1) #("a" "b" "c") string->symbol) + (lens-transform (string-ref-lens 3) "Hello!" char-upcase)) + +@section[#:tag "streams-guide"]{Streams} + +@see-reference-note["streams-reference"]{stream lenses} + +Racket's @tech[#:doc '(lib "scribblings/reference/reference.scrbl")]{streams} contain ordered data, +much like lists, but unlike lists, they are @emph{lazy}. Lenses on streams are similarly lazy, only +forcing the stream up to what is necessary. This allows stream lenses to successfully operate on +infinite streams. + +@(lens-interaction + (lens-view (stream-ref-lens 10) + (stream-map (curry expt 2) (in-naturals)))) + +Keep in mind that since @racket[lens-transform] is strict, using it to update a value within a stream +will force the stream up to the position of the element being modified. diff --git a/lens/private/stream.scrbl b/lens/private/stream.scrbl index 3714da0..edf9a57 100644 --- a/lens/private/stream.scrbl +++ b/lens/private/stream.scrbl @@ -3,7 +3,9 @@ @(require "doc-util/main.rkt") -@title{Stream Lenses} +@title[#:tag "streams-reference"]{Stream Lenses} + +@see-guide-note["streams-guide"]{stream lenses} @defthing[stream-first-lens lens?]{ A lens for viewing the first element of a stream. diff --git a/lens/private/string.scrbl b/lens/private/string.scrbl index c126c74..bb33d85 100644 --- a/lens/private/string.scrbl +++ b/lens/private/string.scrbl @@ -2,7 +2,7 @@ @(require "doc-util/main.rkt") -@title{String Lenses} +@title[#:tag "strings-reference"]{String Lenses} @defproc[(string-ref-lens [i exact-nonnegative-integer?]) lens?]{ Returns a lens for viewing the @racket[i]th character of a string. diff --git a/lens/private/vector/main.scrbl b/lens/private/vector/main.scrbl index ef1ecc5..b83e20c 100644 --- a/lens/private/vector/main.scrbl +++ b/lens/private/vector/main.scrbl @@ -2,7 +2,7 @@ @(require "../doc-util/scribble-include-no-subsection.rkt") -@title{Vector lenses} +@title[#:tag "vectors-reference"]{Vector lenses} @scribble-include/no-subsection["ref.scrbl"] @scribble-include/no-subsection["nested.scrbl"]