56: Write more text in rewrite-type.lp2.rkt

This commit is contained in:
Georges Dupéron 2015-11-10 17:51:55 +01:00
parent e8ab36d022
commit 116d16f74e
3 changed files with 234 additions and 146 deletions

View File

@ -0,0 +1,34 @@
#lang scribble/lp2
@(require "../lib/doc.rkt")
@doc-lib-setup
@title[#:style manual-doc-style]{Graph library}
@(table-of-contents)
@section{Introduction}
@section{Conclusion}
@chunk[<*>
(begin
(module main typed/racket
(require (for-syntax syntax/parse
racket/syntax
"../lib/low-untyped.rkt")
"../lib/low.rkt")
)
(require 'main)
(provide (all-from-out 'main))
(module* test typed/racket
(require (submod "..")
typed/rackunit)
(require (submod ".." doc))))]

View File

@ -4,6 +4,7 @@
@title[#:style manual-doc-style]{Rewriting data structures and their types}
@section[#:tag "sec:intro-example"]{Introductory example}
This module allows purely functional substitution inside a data structure of
arbitrarily deep elements of a given type, while also computing the type of the
result.
@ -11,15 +12,42 @@ result.
For example, one could replace all strings in a data structure by their length:
@CHUNK[<test-example>
(make-replace test-example
(Vectorof (U (List 'tag1 String) (List 'tag2 Number)))
[String Number string-length])]
The result's type would be derived from the original one, but all occurrences of
@tc[String] have been replaced by @tc[Number]. The result itself would have the
value returned by string-length instead of each string, everything else being
identical.
@CHUNK[<test-example>
(check-equal?
(ann (test-example '#((tag1 "a") (tag2 7) (tag1 "bcd")))
(Vectorof (U (List 'tag1 Number) (List 'tag2 Number))))
'#((tag1 1) (tag2 7) (tag1 3)))]
In this example, we used @tc[make-replace], a test macro defined below which
relies on the lower-level utilities provided by this module, namely
@tc[replace-in-type] and @tc[replace-in-instance].
@CHUNK[<test-make-replace>
(define-syntax (make-replace stx)
(syntax-case stx ()
[(_ name type . replace)
#`(begin
(: name ( type #,(replace-in-data-structure #'type #'replace)))
(: name ( type #,(replace-in-type #'type #'replace)))
(define (name v)
#,(replace-in-instance #'v #'type #'replace)))]))
(make-replace test1
#,(replace-in-instance #'v #'type #'replace)))]))]
@subsection{A bigger example}
We would expect this to work on bigger types without any extra efforts. In the
following example, we replace all strings with their length, on a bigger
example:
@CHUNK[<test-big>
(make-replace test-big
(List (Pairof (U (List 'tag1 (List (Vector Symbol)
Number
(Listof String)))
@ -27,10 +55,14 @@ For example, one could replace all strings in a data structure by their length:
Number
(Listof String))))
String))
[String Number string-length])
[String Number string-length])]
The replacement function @tc[test-big] defined above will, as expected, have a
return type containing no more strings, and the correct return value.
@CHUNK[<test-big>
(check-equal?
(ann (test1 '(((tag2 (#(sym) 7 ("ab" "abc" "abcd"))) . "a")))
(ann (test-big '(((tag2 (#(sym) 7 ("ab" "abc" "abcd"))) . "a")))
(List (Pairof (U (List 'tag1 (List (Vector Symbol)
Number
(Listof Number)))
@ -40,84 +72,142 @@ For example, one could replace all strings in a data structure by their length:
Number)))
'(((tag2 (#(sym) 7 (2 3 4))) . 1)))]
@CHUNK[<replace-in-data-structure>
(define-for-syntax (replace-in-data-structure t r)
(define (recursive-replace new-t) (replace-in-data-structure new-t r))
@section{Replacing parts of a type}
The @tc[replace-in-type] @tc[for-syntax] function is pretty straightforward: it
checks whether the given type matches one of the substitution rules given in
@tc[r]. If no substitution rule was found, it matches the type against a small
set of known type constructors like @tc[List] or @tc[Pairof], and recursively
calls itself on the components of the type.
@CHUNK[<replace-in-type>
(define-for-syntax (replace-in-type t r)
(define (recursive-replace new-t) (replace-in-type new-t r))
(define/with-syntax ([from to fun] ...) r)
(syntax-parse t
[x:id
#:attr assoc-from-to (cdr-stx-assoc #'x #'((from . to) ...))
#:when (attribute assoc-from-to)
#'assoc-from-to]
[((~literal List) a ...)
#`(List #,@(stx-map recursive-replace #'(a ...)))]
[((~literal Pairof) a b)
#`(Pairof #,(recursive-replace #'a) #,(recursive-replace #'b))]
[((~literal Listof) a)
#`(Listof #,(recursive-replace #'a))]
[((~literal Vector) a ...)
#`(Vector #,@(stx-map recursive-replace #'(a ...)))]
[((~literal Vectorof) a)
#`(Vectorof #,(recursive-replace #'a))]
[((~literal U) a ...)
#`(U #,@(stx-map recursive-replace #'(a ...)))]
[((~literal quote) a)
;; TODO: if the quoted type is a primitive, we should replace it too
#`(quote a)]
[x:id #'x]))]
<replace-in-type-substitute>
<replace-in-type-other-cases>))]
The clause that matches the type against the substitution rules uses the
@tc[stx-assoc] function defined in our library, which uses
@tc[free-identifier=?] to find the first pair which @tc[car] or @tc[stx-car]
matches the given type @tc[#'x].
@CHUNK[<replace-in-type-substitute>
[x:id
#:attr assoc-from-to (cdr-stx-assoc #'x #'((from . to) ...))
#:when (attribute assoc-from-to)
#'assoc-from-to]]
The other cases use @tc[~literal] and a syntax pattern to find uses of
@tc[List], @tc[Pairof], @tc[Vectorof] etc.
@CHUNK[<replace-in-type-other-cases>
[((~literal List) a ...)
#`(List #,@(stx-map recursive-replace #'(a ...)))]
[((~literal Pairof) a b)
#`(Pairof #,(recursive-replace #'a) #,(recursive-replace #'b))]
[((~literal Listof) a)
#`(Listof #,(recursive-replace #'a))]
[((~literal Vector) a ...)
#`(Vector #,@(stx-map recursive-replace #'(a ...)))]
[((~literal Vectorof) a)
#`(Vectorof #,(recursive-replace #'a))]
[((~literal U) a ...)
#`(U #,@(stx-map recursive-replace #'(a ...)))]
<replace-in-type-case-quote>
[x:id
#'x]]
TODO: If the type is a quoted primitive, we should replace it too, for example
@tc['("abc" symbol)] should be transformed into @tc['(3 symbol)] if we apply the
@tc[[String Number string-length]] substitution from the example in section
@secref{sec:intro-example}.
@CHUNK[<replace-in-type-case-quote>
[((~literal quote) a)
#`(quote a)]]
@section{Replacing parts of an instance}
The @tc[replace-in-instance] for-syntax function is defined in a similar way,
with an internal definition for @tc[recursive-replace]. The case of unions is
offloaded to a separate subroutine.
@CHUNK[<replace-in-instance>
(define-for-syntax (replace-in-instance val t r)
(define/with-syntax ([from to fun] ...) r)
<recursive-replace-in-instance>
<replace-in-union>
(define (recursive-replace stx-val type)
(define/with-syntax val stx-val)
(define/with-syntax (v-cache) (generate-temporaries #'(val-cache)))
(syntax-parse type
[x:id
#:attr assoc-from-to (cdr-stx-assoc #'x
#'((from . (to . fun)) ...))
#:when (attribute assoc-from-to)
#:with (to-type . to-fun) #'assoc-from-to
(define/with-syntax (tmp) (generate-temporaries #'(x)))
;; TODO: Add predicate for to-type in the pattern.
#`(to-fun val)]
[((~literal List) a ...)
(define/with-syntax (tmp ...) (generate-temporaries #'(a ...)))
#`(let-values ([(tmp ...) (apply values val)])
(list #,@(stx-map recursive-replace #'(tmp ...) #'(a ...))))]
[((~literal Pairof) a b)
#`(let ([v-cache val])
(cons #,(recursive-replace #'(car v-cache) #'a)
#,(recursive-replace #'(cdr v-cache) #'b)))]
[((~literal Listof) a)
(define/with-syntax (tmp) (generate-temporaries #'(a)))
#`(map (λ ([tmp : a]) #,(recursive-replace #'tmp #'a))
val)]
[((~literal Vector) a ...)
(define/with-syntax (tmp ...) (generate-temporaries #'(a ...)))
(define/with-syntax (idx ...) (generate-indices #'(a ...)))
#`(let ([v-cache val])
(let ([tmp (vector-ref v-cache idx)]
...)
(vector #,@(stx-map recursive-replace
#'(tmp ...)
#'(a ...)))))]
[((~literal Vectorof) a)
(define/with-syntax (tmp) (generate-temporaries #'(a)))
#`(vector-map (λ ([tmp : a]) #,(recursive-replace #'tmp #'a))
val)]
[((~literal U) a ...)
#`(let ([v-cache val])
(cond
#,@(stx-map (λ (ta) (replace-in-union #'v-cache ta r))
#'(a ...))))]
[((~literal quote) a)
#'val]
[x:id
#'val]))
(recursive-replace val t))]
The @tc[recursive-replace] internal function defined below takes a type
@tc[type] and produces an expression that transforms instances of that type
using the substitution rules given in the parameter @tc[r] of the enclosing
function.
The expression assumes that the instance to be transformed is located in the
variable or expression @tc[stx-val], and caching is used where needed to avoid
evaluating @tc[stx-val] twice. Here is the case that handles @tc[Pairof], which
caches @tc[val] and calls @tc[recursive-replace] with the @tc[car] and @tc[cdr]
as expressions:
@CHUNK[<replace-in-instance-case-pairof>
[((~literal Pairof) a b)
#`(let ([v-cache val])
(cons #,(recursive-replace #'(car v-cache) #'a)
#,(recursive-replace #'(cdr v-cache) #'b)))]]
The other cases are similarly defined:
@CHUNK[<recursive-replace-in-instance>
(define (recursive-replace stx-val type)
(define/with-syntax val stx-val)
(define/with-syntax (v-cache) (generate-temporaries #'(val-cache)))
(syntax-parse type
[x:id
#:attr assoc-from-to (cdr-stx-assoc #'x #'((from . (to . fun)) ...))
#:when (attribute assoc-from-to)
#:with (to-type . to-fun) #'assoc-from-to
(define/with-syntax (tmp) (generate-temporaries #'(x)))
;; TODO: Add predicate for to-type in the pattern.
#`(to-fun val)]
[((~literal List) a ...)
(define/with-syntax (tmp ...) (generate-temporaries #'(a ...)))
#`(let-values ([(tmp ...) (apply values val)])
(list #,@(stx-map recursive-replace #'(tmp ...) #'(a ...))))]
<replace-in-instance-case-pairof>
[((~literal Listof) a)
(define/with-syntax (tmp) (generate-temporaries #'(a)))
#`(map (λ ([tmp : a]) #,(recursive-replace #'tmp #'a))
val)]
[((~literal Vector) a ...)
(define/with-syntax (tmp ...) (generate-temporaries #'(a ...)))
(define/with-syntax (idx ...) (generate-indices #'(a ...)))
#`(let ([v-cache val])
(let ([tmp (vector-ref v-cache idx)]
...)
(vector #,@(stx-map recursive-replace
#'(tmp ...)
#'(a ...)))))]
[((~literal Vectorof) a)
(define/with-syntax (tmp) (generate-temporaries #'(a)))
#`(vector-map (λ ([tmp : a]) #,(recursive-replace #'tmp #'a))
val)]
[((~literal U) a ...)
#`(let ([v-cache val])
(cond
#,@(stx-map (λ (ta) (replace-in-union #'v-cache ta r))
#'(a ...))))]
[((~literal quote) a)
#'val]
[x:id
#'val]))]
For unions, we currently support only tagged unions, that is unions where each
possible type is a @tc[List] with a distinct @tc[tag] in its first element.
TODO: we currently don't check that each @tc[tag] is distinct.
@CHUNK[<replace-in-union>
(define (replace-in-union stx-v-cache t r)
(define/with-syntax v-cache stx-v-cache)
@ -126,76 +216,34 @@ For example, one could replace all strings in a data structure by their length:
<replace-in-tagged-union-instance>]
[_ (error "Type-replace on untagged Unions isn't supported yet!")]))]
For cases of the union which are a tagged list, we use a simple guard, and call
@tc[recursive-replace] on the whole @tc[(List 'tag b ...)] type.
@CHUNK[<replace-in-tagged-union-instance>
#`[(and (list? v-cache) (eq? 'tag (car v-cache)))
#`[(and (list? v-cache)
(not (null? v-cache))
(eq? 'tag (car v-cache)))
#,(recursive-replace #'v-cache t)]]
Handling freer forms of unions causes some problems:
@CHUNK[<replace-in-instance_old>
(define-for-syntax (replace-in-instance val t r)
(define/with-syntax ([from to fun] ...) r)
(define (recursive-replace stx-val type)
(define/with-syntax val stx-val)
(syntax-parse type
[x:id
#:attr assoc-from-to (cdr-stx-assoc #'x
#'((from . (to . fun)) ...))
#:when (attribute assoc-from-to)
#:with (to-type . to-fun) #'assoc-from-to
(define/with-syntax (tmp) (generate-temporaries #'(x)))
;; TODO: Add predicate for to-type in the pattern.
#`(match-abort val [(and tmp) (protected (to-fun tmp))])]
[((~literal List) a ...)
(define/with-syntax (tmp1 ...) (generate-temporaries #'(a ...)))
(define/with-syntax (tmp2 ...) (generate-temporaries #'(a ...)))
(define/with-syntax (rec ...)
(stx-map recursive-replace #'(tmp1 ...) #'(a ...)))
#`(match-abort val
[(list tmp1 ...)
(let-abort ([tmp2 rec] ...)
(protected (list (unprotect tmp2) ...)))])]
[((~literal Pairof) a b)
(define/with-syntax (tmpa1 tmpb1) (generate-temporaries #'(a b)))
(define/with-syntax (tmpa2 tmpb2) (generate-temporaries #'(a b)))
(define/with-syntax reca (recursive-replace #'tmpa1 #'a))
(define/with-syntax recb (recursive-replace #'tmpb1 #'b))
#'(match-abort val
[(cons tmpa1 tmpb1)
(let-abort ([tmpa2 reca] [tmpb2 recb])
(protected (cons (unprotect tmpa2)
(unprotect tmpb2))))])]
#| TODO: |#
[((~literal Listof) a)
(define/with-syntax (tmp1) (generate-temporaries #'(a)))
(define/with-syntax (tmp1x) (generate-temporaries #'(a)))
(define/with-syntax rec (recursive-replace #'tmp1x #'a))
#'(match-abort val
[(list tmp1 (... ...))
(map-abort tmp1 tmp1x rec)])]
[((~literal Vector) a ...)
(define/with-syntax (tmp1 ...) (generate-temporaries #'(a ...)))
(define/with-syntax (tmp2 ...) (generate-temporaries #'(a ...)))
(define/with-syntax (rec ...)
(stx-map recursive-replace #'(tmp1 ...) #'(a ...)))
#`(match-abort val
[(vector tmp1 ...)
(let-abort ([tmp2 rec] ...)
(protected (list (unprotect tmp2) ...)))])]
#| TODO:
[((~literal Vectorof) a)
#`(Vectorof #,(recursive-replace #'a))]
;|#
[((~literal U) a ...)
(define/with-syntax (tmp1 ...) (generate-temporaries #'(a ...)))
(define/with-syntax (rec ...)
(stx-map recursive-replace #'(tmp1 ...) #'(a ...)))
#`(match-abort val
[tmp1 rec]
...)]
[x:id
#'(protected val)]))
;; TODO: if we recieve a 'continue or 'break, give a type error.
#`(unprotect #,(recursive-replace val t)))]
@itemlist[
@item{There are some types without make-predicate, so we can't use
@racket[(make-predicate T)] to know what case of the union we are in}
@item{There are some types for which there is no predicate, like function types
with the same arity}
@item{There are some types for which the type system doesn't acknowledge the
predicates, e.g. @racket[(U (Vector Number) (Vector String String))]: we can't
even bound the type of @racket[(vector-ref x 0)] in that case, it defaults to
@racket[Any].}]
These issues and possible solutions are addressed in more
detail in the
@hyperlink[(string-append "https://phc.fogbugz.com/f/cases/54/"
"Rethink-how-to-do-the-multi-step-types-more-inside")]
{FogBugz case 54}.
@section{Conclusion}
@chunk[<*>
(begin
@ -209,10 +257,10 @@ For example, one could replace all strings in a data structure by their length:
"../type-expander/multi-id.lp2.rkt"
"../type-expander/type-expander.lp2.rkt"
"cond-abort.rkt")
(begin-for-syntax (provide replace-in-data-structure
(begin-for-syntax (provide replace-in-type
replace-in-instance))
<replace-in-data-structure>
<replace-in-type>
<replace-in-instance>)
(require 'main)
@ -227,6 +275,8 @@ For example, one could replace all strings in a data structure by their length:
"../type-expander/type-expander.lp2.rkt"
"cond-abort.rkt")
<test-make-replace>
<test-example>
<test-big>
(require (submod ".." doc))))]

View File

@ -30,12 +30,16 @@ Blah @math{n}, as described by M@._ Foo@.__
(define (foo)
(syntax-e #`#,"foo"))]
@itemlist[
@item{Item 1}
@item{Item 2}]
@(define to-insert 42)
@chunk[<*>
;(displayln #,to-insert) ;; Should work.
(provide foo)
<foo>
(module* test racket
(require (submod ".."))
(require rackunit)
@ -56,4 +60,4 @@ But we would actually want:
(define-syntax-rule (double x) -- should be greyed out
(let ((x-cache x))
(+ x-cache x-cache))) -- everything except the changed bits should
-- be greyed out]
-- be greyed out]