Renamed files, added lib.rkt (FB case 69).

This commit is contained in:
Georges Dupéron 2016-01-23 13:12:50 +01:00
parent d6c6f86544
commit 4cceadc84e
12 changed files with 2191 additions and 2165 deletions

View File

@ -8,8 +8,9 @@
#%module-begin)
(rename-out #;[new-#%top #%top]
[new-#%module-begin #%module-begin]))
(require "graph4.lp2.rkt"
(require "get.lp2.rkt"
"../lib/low-untyped.rkt"
(for-syntax racket/string
syntax/parse
@ -46,7 +47,7 @@
(define-syntax (new-#%module-begin stx)
(syntax-case stx ()
[(_ . body)
[(_ . body)
#`(#%module-begin
. #,(fold-syntax replace-dots
#'body))]))
@ -108,10 +109,10 @@
(module test (submod ".." dotlang)
(require typed/rackunit
"../lib/low.rkt"
"graph4.lp2.rkt"
(submod "graph3.lp2.rkt" test)
"map4.rkt")
"get.lp2.rkt"
(submod "graph.lp2.rkt" test)
"map.rkt")
(let ((foo..bar 42))
(check-equal?: foo..bar 42))

View File

@ -205,7 +205,7 @@ The type for the function generated by @tc[λget] mirrors the cases from
"../lib/low.rkt"
"structure.lp2.rkt"
"variant.lp2.rkt"
"graph3.lp2.rkt"
"graph.lp2.rkt"
"../type-expander/multi-id.lp2.rkt"
"../type-expander/type-expander.lp2.rkt"
"map1.rkt")

File diff suppressed because it is too large Load Diff

View File

@ -1,655 +0,0 @@
#lang debug scribble/lp2
@(require "../lib/doc.rkt")
@doc-lib-setup
@(define (comment . _) "")
@title[#:style manual-doc-style]{Graph library}
@(table-of-contents)
@; TODO: allow a mapping to return a new placeholder, in order to act as a
@; redirect. All references to the old placeholder will act as if they were to
@; the new placeholder.
@section{Introduction}
This module provides a @tc[graph] macro which helps constructing immutable
graphs (using lambdas to defer potentially cyclic references).
@subsection{Example usage}
We will start with a running example, which will help us both show the macro's
syntax, and see some of the key advantages offered by this graph library.
@subsection{The graph's type}
Each node type in the graph is a variant's constructor, tagged with the node
name. For example, a graph representing a city and its inhabitants could use
these constructors:
@chunk[<example-variants>
[City [streets : (Listof Street)] [people : (Listof Person)] <m-city>]
[Street [sname : String] [houses : (Listof House)] <m-street>]
[House [owner : Person] [location : Street] <m-house>]
[Person [name : String] <m-person>]]
Notice the cycle in the type: a street contains houses, which are located on the
same street.
@subsubsection{A seed from which to unravel the graph: the root parameters}
In order to build a graph with that type, we start from the root parameters.
Here, we will take a representation of the city as a list of
@tc[(street-name . person-name)] pairs, and will convert it to a more convenient
graph representation. Our single root parameter will thus be the whole list:
@chunk[<example-root>
'(["Amy" . "Ada Street"]
["Jack" . "J Street"]
["Anabella" . "Ada Street"])]
We will then provide a mapping from the root parameter to the root node, in our
case @tc[City]. When processing the root parameter, one can call other mappings
that will create their corresponding nodes.
@subsubsection{Mapping the root parameters to the root node}
Here is the root mapping for our example. It maps over the list of names and
street names @tc[c], and calls for each element the @tc[m-street] and
@tc[m-person] mappings.
@; Would be nicer with (map (∘ (curry street c) car) c)), but that doesn't
@; typecheck (yet).
@chunk[<m-city>
[(m-city [c : (Listof (Pairof String String))])
(City (remove-duplicates (map (curry m-street c) (cdrs c)))
(remove-duplicates (map m-person (cars c))))]]
@subsubsection{More mappings}
Next, we write the @tc[m-street] mapping, which takes a street name and the
whole city @tc[c] in list form, and creates a @tc[Street] node.
@chunk[<m-street>
[(m-street [c : (Listof (Pairof String String))] [s : String])
(Street s (map (curry (curry m-house s) c)
(cars (filter (λ ([x : (Pairof String String)])
(equal? (cdr x) s))
c))))]]
The @tc[m-house] mapping defined below calls back the @tc[m-street] mapping, to
store for each house a reference to the containing street. Normally, this would
cause infinite recursion in an eager language, like @tc[typed/racket]. However,
the mappings aren't called directly, and instead, in the body of @tc[m-house],
@tc[m-street] is shadowed by a function which returns a placeholder. This allows
us to not worry about mutually recursive mappings: a mapping can be called any
number of times with the same data, it will actually only be run once.
The @tc[define-graph] macro will post-process the result of each mapping, and
replace the placeholders with promises for the the result of the mapping. The
promises are not available during graph construction, so there is no risk of
forcing one before it is available.
We can now write the @tc[m-house] and @tc[m-person] mappings.
@chunk[<m-house>
[(m-house [s : String]
[c : (Listof (Pairof String String))]
[p : String])
(House (m-person p) (m-street c s))]]
@chunk[<m-person>
[(m-person [p : String])
(Person p)]]
@subsubsection{Creating an instance of the graph}
For now, we will supply directly the root arguments to the @tc[define-graph]
macro, as well as the node types and mappings. We can later curry the macro, so
that it first takes the node types and mappings, and produces a lambda taking
the root arguments as parameters.
@chunk[<use-example>
(define-graph make-g <example-variants>)
#;(define g (make-g <example-root>))
(define g1 (make-g <example-root>))
(define g g1)]
@subsection{More details on the semantics}
Let's take a second look at the root mapping:
@chunk[<m-city-2>
[(m-city [c : (Listof (Pairof String String))])
(City (remove-duplicates (map (curry m-street c) (cars c)))
(remove-duplicates (map m-person (cdrs c))))]]
As this example shows, we can use @tc[m-street] as any other function, passing
it to @tc[curry], and calling @tc[remove-duplicates] on the results. Note that
each placeholder returned by @tc[m-street] will contain all information passed
to it, here a street name and @tc[c]. Two placeholders for @tc[m-street] will
therefore be @tc[equal?] if and only if all the arguments passed to
@tc[m-street] are @tc[equal?]. The placeholders also include a symbol specifying
which mapping was called, so two placeholders for two different mappings will
not be @tc[equal?], even if identical parameters were supplied.
The node type allowing placeholders is derived from the ideal type given above.
Here, the type for @tc[Person] is @tc[[Person [name : String]]], so there are no
substitutions to make. Conversely, the type for @tc[City], originally expressed
as @tc[[(Listof Street) (Listof Person)]], will be rewritten as
@tc[[(Listof Street/placeholder-type) (Listof Person/placeholder-type)]].
The @tc[rewrite-type] module, which we use to derive types with placeholders
from the ideal ones, only handles a handful of the types offered by
@tc[typed/racket]. In particular, it does not handle recursive types described
with @tc[Rec] yet.
@section{Implementation}
In this section, we will describe how the @tc[define-graph] macro is
implemented.
@subsection{The macro's syntax}
We use a simple syntax for @tc[define-graph], and make it more flexible through
wrapper macros.
@chunk[<signature>
(define-graph name
[node <field-signature> <mapping-declaration>]
)]
Where @tc[<field-signature>] is:
@chunk[<field-signature>
[field:id :colon field-type:expr]]
And @tc[<mapping-declaration>] is:
@chunk[<mapping-declaration>
((mapping:id [param:id :colon param-type:expr] )
. mapping-body)]
@subsection{The different types of a node}
A single node name can refer to several types:
@itemlist[
@item{The @emph{ideal} type, expressed by the user, for example
@racket[[City (Listof Street) (Listof Person)]], it is never used as-is in
practice}
@item{The @emph{placeholder} type, type and constructor, which just store the
arguments for the mapping along with a tag indicating the node name}
@item{The @emph{incomplete} type, in which references to other node types are
allowed to be either actual (@racket[incomplete]) instances, or placeholders.
For example, @racket[[City (Listof (U Street Street/placeholder-type))
(Listof (U Person Person/placeholder-type))]].}
@item{The @emph{with-indices} type, in which references to other node types
must be replaced by an index into the results list for the target node's
@racket[with-promises] type. For example,
@racket[[City (Listof (List 'Street/with-indices-tag2 Index))
(Listof (List 'Person/with-indices-tag2 Index))]].}
@item{The @emph{with-promises} type, in which references to other node types
must be replaced by a @racket[Promise] for the target node's
@racket[with-promises] type. For example,
@racket[[City (Listof (Promise Street/with-promises-type))
(Listof (Promise Person/with-promises-type))]].}
@item{The @emph{mapping function}, which takes some parameters and
returns a node (using the code provided by the user)}]
We derive identifiers for these based on the @tc[node] name:
@;;;;
@chunk[<define-ids>
(define/with-syntax ((root-param ) . _) #'((param ) ))
(define/with-syntax ((root-param-type ) . _) #'((param-type ) ))
(define-temp-ids "~a/make-placeholder" (node ) #:first-base root)
(define-temp-ids "~a/placeholder-type" (node ))
(define-temp-ids "~a/placeholder-tag" (node ))
(define-temp-ids "~a/placeholder-queue" (node ))
(define-temp-ids "~a/incomplete-type" (node ))
(define-temp-ids "~a/make-incomplete" (node ))
(define-temp-ids "~a/incomplete-tag" (node ))
(define-temp-ids "~a/with-indices-type" (node ))
(define-temp-ids "~a/make-with-indices" (node ))
(define-temp-ids "~a/with-indices-tag" (node ))
(define-temp-ids "~a/with-indices-tag2" (node ))
(define-temp-ids "~a/index-type" (node ))
(define-temp-ids "~a/with-indices→with-promises" (node )
#:first-base root)
(define-temp-ids "~a/with-promises-type" (node ) #:first-base root)
(define-temp-ids "~a/make-with-promises" (node ))
(define-temp-ids "~a/with-promises-tag" (node ))
(define-temp-ids "~a/mapping-function" (node ))
(define-temp-ids "~a/database" (node ) #:first-base root)
(define-temp-ids "~a/value" ((field ) ))]
@subsection{Overview}
The macro relies heavily on two sidekick modules: @tc[rewrite-type], and
@tc[fold-queue]. The former will allow us to derive from the ideal type of a
node the incomplete type and the with-promises type. It will also allow us to
search inside instances of incomplete nodes, in order to extract the
placehoders, and replace these parts with promises. The latter, @tc[fold-queue],
will be used to process all the pending placeholders, with the possibility to
enqueue more as new placeholders are discovered inside incomplete nodes.
When the graph constructor is called with the arguments for the root parameters,
it is equivalent to make and then resolve an initial placeholder. We will use a
function from the @tc[fold-queue] library to process the queues of pending
placeholders, starting with a queue containing only that root placeholder.
We will have one queue for each placeholder type.@note{It we had only one queue,
we would have only one collection of results, and would need a @racket[cast]
when extracting nodes from the collection of results.} The element types of the
queues will therefore be these placeholder types.
@chunk[<fold-queue-type-element>
node/placeholder-type]
The return type for each queue will be the corresponding with-indices type. The
fold-queues function will therefore return a vector of with-indices nodes for
each node type.
@chunk[<fold-queue-type-result>
node/with-indices-type]
@; Problem: how do we ensure we return the right type for the root?
@; How do we avoid casts when doing look-ups?
@; We need several queues, handled in parallel, with distinct element types.
@; * Several result aggregators, one for each type, so we don't have to cast
@; * Several queues, so that we can make sure the root node is of the expected
@; type.
@; TODO: clarity.
@; The @tc[fold-queues] function allows us to associate each element with a tag,
@; so that, inside the processing function and outside, we can refer to an
@; element using this tag, which can be more lightweight than keeping a copy of
@; the element.
@;
@; We will tag our elements with an @tc[Index], which prevents memory leakage:
@; if we kept references to the original data added to the queue, a graph's
@; representation would hold references to its input, which is not the case when
@; using simple integers to refer to other nodes, instead of using the input for
@; these nodes. Also, it makes lookups in the database much faster, as we will
@; be able to use an array instead of a hash table.
@subsection{The queues of placeholders}
The fold-queues macro takes a root element, in our case the root placeholder,
which it will insert into the first queue. The next clauses are the queue
handlers, which look like function definitions of the form
@tc[(queue-name [element : element-type] Δ-queues enqueue) : result-type]. The
@tc[enqueue] argument is a function used to enqueue elements and get a tag in
return, which can later be used to retrieve the processed element.
Since the @tc[enqueue] function is pure, it takes a parameter of the same type
as @tc[Δ-queues] representing the already-enqueued elements, and returns a
modified copy, in addition to the tag. The queue's processing body should return
two values: the result of processing the element, and the latest version of
@tc[Δ-queues], which stores the new elements to be added to the queue.
@chunk[<fold-queues>
(fold-queues <root-placeholder>
[(node/placeholder-queue [e : <fold-queue-type-element>]
[Δ-queues : Δ-Queues]
enqueue)
: <fold-queue-type-result>
<fold-queue-body>]
...)]
@subsection{Making placeholders for nodes}
We start creating the root placeholder which we provide to @tc[fold-queues].
@chunk[<root-placeholder>
(root/make-placeholder root-param )]
To make the placeholder, we will need a @tc[node/make-placeholder] function for
each @tc[node]. We first define the type of each placeholder (a list of
arguments, tagged with the @tc[node]'s name):
@; TODO: maybe replace node types with placeholder types
@chunk[<define-placeholder-type>
(define-type node/placeholder-type
(List 'node/placeholder-tag
param-type ))]
@; TODO: just use (variant [mapping param-type ...] ...)
Then we define the @tc[node/make-placeholder] function:
@chunk[<define-make-placeholder>
(: node/make-placeholder ( param-type node/placeholder-type))
(define (node/make-placeholder param )
(list 'node/placeholder-tag param ))]
@subsection{Making with-indices nodes}
We derive the @tc[with-indices] type from each @emph{ideal} node type using the
@tc[tmpl-replace-in-type] template metafunction from the rewrite-type library.
We replace all occurrences of a @tc[node] name with an @tc[Index], which
indicates at which index in the queue's results the successor can be found.
@; TODO: use a type-expander here, instead of a template metafunction.
@CHUNK[<define-with-indices>
(define-type node/index-type (List 'node/with-indices-tag2 Index))
(define-type node/with-indices-type
(List 'node/with-indices-tag <field/with-indices-type> ))
(: node/make-with-indices ( <field/with-indices-type>
node/with-indices-type))
(define (node/make-with-indices field )
(list 'node/with-indices-tag field ))]
@CHUNK[<field/with-indices-type>
(tmpl-replace-in-type field-type [node node/index-type] )]
@subsection{Making with-promises nodes}
We derive the @tc[with-promises] type from each @emph{ideal} node type using
the @tc[tmpl-replace-in-type] template metafunction from the rewrite-type
library. We replace all occurrences of a @tc[node] name with a @tc[Promise] for
that node's @tc[with-promises] type.
@; TODO: use a type-expander here, instead of a template metafunction.
@CHUNK[<define-with-promises>
(define-type node/with-promises-type
(tagged node/with-promises-tag
[field : <field/with-promises-type>] ))
(: node/make-with-promises ( <field/with-promises-type>
node/with-promises-type))
(define (node/make-with-promises field/value )
(tagged node/with-promises-tag
[field : <field/with-promises-type> field/value]
))]
@CHUNK[<field/with-promises-type>
(tmpl-replace-in-type field-type
[node (Promise node/with-promises-type)] )]
@subsection{Making incomplete nodes}
We derive the @tc[incomplete] type from each @emph{ideal} node type using
the @tc[tmpl-replace-in-type] template metafunction from the rewrite-type
library. We replace all occurrences of a @tc[node] name with its
@tc[placeholder] type.
@; TODO: use a type-expander here, instead of a template metafunction.
@CHUNK[<define-incomplete>
(define-type node/incomplete-type
(List 'node/incomplete-tag <field/incomplete-type> ))
(: node/make-incomplete ( <field/incomplete-type>
node/incomplete-type))
(define (node/make-incomplete field )
(list 'node/incomplete-tag field ))]
@CHUNK[<field/incomplete-type>
(tmpl-replace-in-type field-type
[node node/placeholder-type] )]
@subsection{Converting incomplete nodes to with-indices ones}
@; TODO: we don't need that many annotations
@chunk[<placeholder→with-indices-function>
(λ ([p : node/placeholder-type] [Δ-acc : Δ-Queues])
: (values (List 'node/with-indices-tag2 Index) Δ-Queues)
(% index new-Δ-acc = (enqueue 'node/placeholder-queue p Δ-acc)
(values (list 'node/with-indices-tag2 index)
new-Δ-acc)))]
@chunk[<placeholder→with-indices-clause>
[node/placeholder-type
(List 'node/with-indices-tag2 Index)
(λ (x) (and (pair? x)
(eq? (car x) 'node/placeholder-tag)))
<placeholder→with-indices-function>]]
@subsubsection{Processing the placeholders}
@; TODO: also allow returning a placeholder (which means we should then
@; process that placeholder in turn). The placeholder should return the
@; same node type, but can use a different mapping?
@; Or maybe we can do this from the ouside, using a wrapper macro?
@; TODO: we don't need that many let etc., use % instead once everything works.
@CHUNK[<fold-queue-body>
(let ([mapping-result (apply node/mapping-function (cdr e))])
(let ([f (tmpl-fold-instance (List <field-incomplete-type> )
Δ-Queues
<placeholder→with-indices-clause> )])
(let-values ([(r new-Δ-queues) (f (cdr mapping-result) Δ-queues)])
(values (cons 'node/with-indices-tag r)
new-Δ-queues))))]
Where @tc[<field-incomplete-type>] is the @tc[field-type] in which node types
are replaced by placeholder types:
@chunk[<field-incomplete-type>
(tmpl-replace-in-type field-type
[node node/placeholder-type] )]
@subsection{The mapping functions}
We define the mapping functions as they are described by the user, with an
important change: Instead of returning an @emph{ideal} node type, we expect them
to return an @emph{incomplete} node type.
@chunk[<define-mapping-function>
(: node/mapping-function ( param-type node/incomplete-type))
(define node/mapping-function
(let ([mapping node/make-placeholder]
[node node/make-incomplete]
)
(λ ([param : param-type] ) : node/incomplete-type
. mapping-body)))]
@subsection{Returning a with-promises nodes}
We will return a with-promises version of the root node, which contains promises
for its successors. The user can then force one of these to obtain the
with-promises version of the desired successor.
@; TODO: put a diagram here, or an example at least
This use of promises is safe, since their resolution depends only on the vectors
returned by fold-queues, which are already fully computed when we create the
root with-promises node. We therefore have no risk of forcing a promise that
can't be resolved, or that would depend on itself, causing an infinite loop.
@subsubsection{Why use promises?}
We use promises because we would like to only use immutable data structures.
Resolving the links in the graph would require mutating the nodes, so instead,
when extracting the @emph{placeholders} from an @emph{incomplete} node, we
produce a @emph{with-indices} node, which, instead of direct references to the
successors, just stores a tag and index. Later, the successors are processed,
and stored at the corresponding index in the queue for that tag.
We then wrap each tagged index with a lambda, which also holds a reference to
the vectors returned by fold-queue, which containin all the with-indices nodes.
When calling the lambda, it extracts the with-indices node for that tag and
index, further replaces the tagged indices within, and returns a brand new
with-promises node.
We could leave it as that, having the with-promises nodes contain lambdas
instead of actual references to their successors. However, when an immutable
function (like one of these lambdas) is called twice with the same arguments (in
this case none), @tc[typed/racket]'s occurrence typing currently does not infer
that the result will always be the same. This means that pattern-matching using
the @tc[match] macro won't work properly, for example. We therefore wrap these
functions into promises. The occcurrence typing mechanism in @tc[typed/racket]
knows that a promise will always return the same value when forced multiple
times. By default, promises use mutable data structures under the hood, to cache
their result, but we do not rely on that. We could use @tc[delay/name], which
doesn't cache the return value, but it was removed from @tc[typed/racket]
because @hyperlink["https://github.com/racket/typed-racket/issues/159"]{it
caused type safety problems}.
@subsubsection{Creating with-promises nodes from with-indices ones}
@chunk[<index→promise-clause>
[node/index-type
(Promise node/with-promises-type)
(λ (x) (and (pair? x)
(eq? (car x) 'node/with-indices-tag2)))
(λ ([tagged-index : node/index-type] [acc : Void])
: (values (Promise node/with-promises-type) Void)
(values <index→promise> acc))]]
TODO: check what we are closing over in that promise.
I think we are closing over the successor-with-index (but not its whole
database), as well as everything that the with-indices→with-promises function
closes over.
@chunk[<index→promise>
(let ([successor-with-index (vector-ref node/database
(cadr tagged-index))])
(delay (node/with-indices→with-promises successor-with-index)))]
@chunk[<define-with-indices→with-promises>
(: node/with-indices→with-promises ( node/with-indices-type
node/with-promises-type))
(define (node/with-indices→with-promises n)
(define f (tmpl-fold-instance (List <field-with-indices-type> )
Void
<index→promise-clause> ))
(apply node/make-with-promises (first-value (f (cdr n) (void)))))]
Where @tc[<field-with-indices-type>] is the @tc[field-type] in which node types
are replaced by tagged indices:
@chunk[<field-with-indices-type>
(tmpl-replace-in-type field-type [node node/index-type] )]
@;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@comment[#|
@subsection{Converting incomplete nodes to with-promises ones}
@chunk[<convert-incomplete-to-with-promises>
[node/incomplete-type
node/with-promises-type
(λ (x) (and (pair? x) (eq? (car x) 'node/incomplete-tag)))
(λ ([x : node/incomplete-type] [acc : Void])
<convert-incomplete-successor>)]]
@chunk[<convert-placeholder-to-with-promises>
[mapping/placeholder-type
(tmpl-replace-in-type result-type [node node/with-promises-type] )
(λ (x) (and (pair? x) (eq? (car x) 'mapping/placeholder-tag)))
(λ ([x : mapping/placeholder-type] [acc : Void])
<convert-placeholder-successor>)]]
@; TODO: this would be much simpler if we forced having only one mapping per
@; node, and extended that with a macro.
@chunk[<define-compatible-mappings>
(define/with-syntax ((node/compatible-mappings ...) ...)
(for/list ([x (in-syntax #'(node ...))])
(multiassoc-syntax
x
#'([result-type . mapping]
))))]
@chunk[<convert-incomplete-successor>
(error (~a "Not implemented yet " x))]
@chunk[<convert-placeholder-successor>
(% index new-Δ-queues = (enqueue 'mapping/placeholder-tag x Δ-queues)
(list 'mapping/placeholder-tag index)
(error (~a "Not implemented yet " x)))]
|#]
@section{Putting it all together}
@chunk[<define-graph>
(define-syntax/parse <signature>
<define-ids>
#|((λ (x) (pretty-write (syntax->datum x)) x)|#
(template
;(let ()
(begin
(begin <define-placeholder-type>)
(begin <define-make-placeholder>)
(begin <define-with-indices>)
(begin <define-with-promises>)
(begin <define-incomplete>)
(begin <define-mapping-function>)
(: name ( root-param-type (Promise root/with-promises-type)))
(define (name root-param )
(match-let ([(list node/database ) <fold-queues>])
(begin <define-with-indices→with-promises>)
(let ([root/with-promises (root/with-indices→with-promises
(vector-ref root/database 0))])
(delay root/with-promises)))))))]
@chunk[<module-main>
(module main typed/racket
(require (for-syntax syntax/parse
racket/syntax
syntax/stx
syntax/parse/experimental/template
racket/sequence
"rewrite-type.lp2.rkt"
"../lib/low-untyped.rkt")
"fold-queues.lp2.rkt"
"rewrite-type.lp2.rkt"
"../lib/low.rkt"
"structure.lp2.rkt"
"variant.lp2.rkt"
"../type-expander/type-expander.lp2.rkt")
;(begin-for-syntax
;<multiassoc-syntax>)
(provide define-graph)
<define-graph>)]
In @tc[module-test], we have to require @tc[type-expander] because it provides a
@tc[:] macro which is a different identifier than the one from typed/racket,
therefore the @tc[:] bound in the @tc[graph] macro with @tc[:colon] would
not match the one from @tc[typed/racket]
@chunk[<module-test>
(module* test typed/racket
(require (submod "..")
"fold-queues.lp2.rkt"; DEBUG
"rewrite-type.lp2.rkt"; DEBUG
"../lib/low.rkt"; DEBUG
"structure.lp2.rkt"; DEBUG
"variant.lp2.rkt"; DEBUG
"../type-expander/type-expander.lp2.rkt"
typed/rackunit)
(provide g)
<use-example>)]
The whole file, finally:
@chunk[<*>
(begin
<module-main>
(require 'main)
(provide (all-from-out 'main))
<module-test>)]

View File

@ -0,0 +1,884 @@
#lang scribble/lp2
@(require "../lib/doc.rkt")
@doc-lib-setup
@title[#:style manual-doc-style]{Graph implementation}
This module provides (a simplified form of) recursive algebraic data structures,
with the ability to handle the structure as a collection of nodes, and process
them all in a way similar to what @tc[map] provides. Traditionally, immutable
data structures can't form cycles, but can easily be traversed to reach all
nodes. Conversely, iterating over a cyclic data structure (created via lazy
evaluation or thunks) is difficult if at all possible.
More formally, this module offers fold operations on heterogeneous, richly typed
graphs.
@(table-of-contents)
@section{Notes on complex transform result types}
We wish at one point to support complex result types for the transforms, instead
of only allowing a single node type.
We have to impose a constraint: do not have a cycle inside the transform's
result that doesn't go through a node, since we break cycles by replacing nodes
with a promise. The safest way to satisfy that constraint is to enforce the
absence of loops at the type level.
We would then inline the called transform's results, breaking the cycles by
replacing nodes with a thunk that returns the desired node. That thunk will be
wrapped into a Promise that calls it, so that typed/racket's occurrence typing
is happy, but we don't rely on the memoization semantics.
@subsection{Compile-time handling of complex transform result types}
During macro-expansion, we generate procedures that process nodes found in
transforms' results, by inlining the results of called transforms. If we find a
@tc[transform/link-request] type in some place we don't know how to rewrite
(like a function type, for example), we throw an error. Similarly, if we
encounter a cycle in the type that does not go through a node type, we throw an
error.
These procedures will help generate code to make a facade node from the
incomplete one. When inlining results from called transforms, they will request
other incomplete nodes from the database.
@subsection{Two-step graph creation}
Writing a graph-generation macro that allows complex return types for transforms
seems difficult, and it would be easier to write a simple graph-generation
macro, that only accepts transforms with return a single node type. We could
build on top of that a more flexible macro, that would first generate a graph
where each transform's result is wrapped in an ad-hoc single-field node. Then,
we would automatically generate a second graph transformation that produces the
desired nodes from that graph.
Example: transform @tc[t1] takes a list of numbers as input, and produces a list
of either calls to transform @tc[t2] or nodes @tc[ni] as output. The @tc[t2]
transform generates a pair of nodes @tc[(ni [x Number])] and
@tc[(nj [y String])].
The user would describe the graph like this:
@chunk[<example-1674389-2>
(make-graph ([root (Listof (U ni (Pairof ni nj)))]
[ni [x Number]]
[nj [y String]])
[(t1 [ln : (Listof Number)] : (Listof (U ni t2))
(map (λ (x) (if (even? x)
(t2 x)
(ni x)))
ln))]
[(t2 [n : Number] : (Pairof ni nj)
(cons (ni n) (nj (format "~a" n))))])]
In the above, the result type of @tc[t1] has to be @tc[(Listof (U ni t2))]
instead of @tc[(Listof (U ni (Pairof ni nj)))], because otherwise we can't
easily automatically infer that @tc[(Pairof ni nj)] was actually @tc[t2],
without looking at the body of the transform. In a more advanced version, we
could substitute every @tc[result-type] found in another transform's
@tc[result-type] by @tc[(U result-type transform/link-request)], however that
would likely produce spurious cycles that do not go through a node, so it's
probably best to make things explicit, and let the user write @tc[U].
@chunk[<example-1674389>
(graph ([r-t1 [result (Listof (U ni t2))]]
[r-t2 [result (Pairof ni nj)]])
[(t1 [ln : (Listof Number)] : r-t1
(r-t1 (map (λ (x) (if (even? x)
(t2 x)
(ni x)))
ln)))]
[(t2 [n : Number] : r-t2
(r-t2 (cons (ni n)
(nj (format "~a" n)))))])]
Then use this graph transform:
@chunk[<example-1674389-2>
(make-graph ([root [result (Listof (Pairof ni nj))]]
[ni [x Number]]
[nj [y String]])
[(r-t1→root [t1 : r-t1]) : root
(root (map (λ (v)
(match v
[(? list?) (r-t2-result v)]
[(ni _) v]))
(r-t1-result t1)))])]
@subsection{Many to one transforms}
This example covers one to many transforms. What about many to one transforms?
The macro we are building allows generating graphs, but does not care about the
input. In the case were transforming a graph of @tc[house]s, @tc[street]s and a
@tc[city], and we want to condense all the @tc[house]s on one side of each
@tc[street] to a @tc[suburb], we would write a transform @tc[t1] for@tc[street]
which passes the whole list of @tc[house]s to a transform @tc[t2]. The @tc[t2]
transform would create a @tc[suburb] from those, without calling a transform for
each @tc[house].
@subsection{Implicit rule names}
In order to allow implicit rule names, when there's only one rule with the
desired result node, we can use the node's name as the transform name. We should
think about naming conflicts: when calling @tc[n], should it insert a link
request for the transform, or should it create an incomplete node?
@subsection[#:tag "graph|complex-transforms-return-type-conclusion"]{Conclusion}
With this approach, we can write the graph creation macro with the guaranty
that the result of a transform always is exactly one node type. More complex
transform result types can be decomposed into to two passes.
A downside is that we can't inspect the result of a call to another transform,
since it's not actually calling it, and we're only getting an opaque link
request back. We couldn't call the other transform anyway, because it could half
of the time return a value immediately, and half of the time call us back (with
the same arguments), causing an infinite loop. For that, we could declare some
#:helper transforms, that get called immediately (but if they run into an
infinite loop it's not our fault).
@section{Comparison with @racket[make-placeholder] and
@racket[make-reader-graph]}
Comparison of this approach with @tc[make-placeholder] and
@tc[make-reader-graph]:
@itemlist[
@item{They don't guarantee at compile-time that you'll fill in all
placeholders. We could use @racket[make-placeholder] and
@racket[make-reader-graph] wrapped inside a macro that makes sure that all
placeholders are filled (same approach as we have).}
@item{I don't think you can iterate over all the nodes or over the nodes of a
specific type, and @racket[make-placeholder] isn't typed (yet) anyway I
guess).}]
@section{Constructor}
Here is an overview of the architecture of the graph constructor:
@itemlist[
@item{We first save the parameter types in the old context, because we later
shadow the node names, and the parameters should refer to the old types.
Depending on how we write the rest, this might not be necessary though, since
it is possible we need to write @racket[(og node)] to refer to nodes types
from the old graph @racket[og].}
@item{We then define the node names as constructors for incomplete types
which means that they can contain link requests for the results other
transforms}
@item{We define data structures representing link requests. Each link request
encapsulates a thunk that performs the transform's work when called, as well
as the name of the transform and its arguments, used to detect when we have
two identical link requests (which can be due to cycles in the resulting
graph, for example).}
@item{We then define the transforms as procedures that return a link request.}]
@chunk[<make-graph-constructor>
(define-syntax/parse
(make-graph-constructor ([node (field:id field-type:expr) ...] ...)
[transform:id (param:id param-type:expr) ...
(~literal :) result-type:id
body ...]
...)
<stx-transform/link-request>
<stx-make-graph-database>
<stx-node/incomplete>
<stx-param-type/old>
<stx-transform/result-node/extract-link-requests>
<stx-transform/link-request→incomplete>
#`(let ()
<param-type/old>
(let ()
<define-incomplete-types>
<define-make-link-requests>
<transform/link-request→incomplete>
<define-transforms>
<make-graph-database>
make-graph-database)))]
@chunk[<test-make-graph-constructor>
(define make-g (make-graph-constructor
([ma (fav String) (faa ma) (fab mb)]
[mb (fbv String) (fba ma)])
[transform-a (s String) : ma
(ma s
(transform-a s)
(transform-b "b"))]
[transform-b (s String) : mb
(mb s
(transform-a s))]))
(make-g "root-arg")]
@subsection{Saving parameter types in old context}
@chunk[<stx-param-type/old>
(define/with-syntax ((param-type/old ...) ...)
(stx-map (λ (ps)
(with-syntax ([(t sps ...) ps])
(format-temp-ids "~a/~a/memorized-type" #'t #'(sps ...))))
#'((transform param ...) ...)))]
@chunk[<param-type/old>
(define-type param-type/old param-type)
...
...]
@subsection{Incomplete nodes}
When a transform returns an object, it is incomplete (it potentially contains
link requests instead of actual references to the nodes).
We prepare some template variables. The first is the name of the tagged variant
representing an incomplete node:
@chunk[<stx-node/incomplete>
(define/with-syntax (node/incomplete ...)
(format-temp-ids "~a/incomplete" #'(node ...)))]
Then, we build a reverse map, which from a node type obtains all the transforms
returning that node type. More specifically, we are interested in the
transform's link request type.
@chunk[<stx-node/incomplete>
(define/with-syntax ((node/link-request-types ...) ...)
(for/list ([x (in-syntax #'(node ...))])
(multiassoc-syntax x
#'([result-type . transform/link-request] ...))))]
The third template variable we define maps transforms to the incomplete type for
their returned node.
@chunk[<stx-node/incomplete>
(define/with-syntax (transform/result-node/incomplete ...)
(for/list ([x (in-syntax #'(result-type ...))])
(assoc-syntax x #'([node . node/incomplete] ...))))]
@CHUNK[<define-incomplete-types>
(define-type node (U node/link-request-types ...)
#:omit-define-syntaxes)
...
(define-tagged node/incomplete [field field-type] ...)
...
(define-multi-id node
#:match-expander-id node/incomplete
#:call-id node/incomplete)
...]
@subsection{Link requests for nodes}
When a transform wants to produce a reference to the result of another transform
of some data, it generates instead a link request, which encapsulates the
desired transform and arguments, without actually performing it.
@chunk[<stx-transform/link-request>
(define/with-syntax (transform/link-request ...)
(format-temp-ids "~a/link-request" #'(transform ...)))]
Due to an issue with @tc[typed/racket] (@tc[struct]s aren't properly declared
inside a @tc[let]), we need to pre-declare the @tc[transform/link-request]
@tc[struct]. Since the call to make-graph could itself be inside a @tc[let], we
need to pre-declare it in this file, instead of declaring it at the top of the
macro.
We're making the structure transparent for easier debugging, but at the time of
writing this, it needs not be.
@chunk[<pre-declare-transform/link-request>
(struct (TKey)
transform/link-request-pre-declared
([key : TKey])
#:transparent)]
@chunk[<define-make-link-requests>
(define-type transform/link-request
(transform/link-request-pre-declared
(List 'transform
param-type/old ...)))
...]
@subsection{Transforms}
@chunk[<stx-transform/link-request→incomplete>
(define/with-syntax (transform/link-request→incomplete ...)
(format-temp-ids "~a/link-request→incomplete" #'(transform ...)))]
@chunk[<transform/link-request→incomplete>
(begin
(: transform/link-request→incomplete
( param-type/old ... transform/result-node/incomplete))
(define (transform/link-request→incomplete param ...)
body ...))
...]
@chunk[<define-transforms>
(begin
(: transform
( param-type/old ... transform/link-request))
(define (transform param ...)
((inst transform/link-request-pre-declared
(List 'transform
param-type/old ...))
(list 'transform param ...))))
...]
@section{Queue}
@chunk[<stx-make-graph-database>
(define/with-syntax (root-transform . _) #'(transform ...))
(define/with-syntax ((root-transform/param-type ...) . _)
#'((param-type ...) ...))
(define/with-syntax ((root-transform/param ...) . _)
#'((param ...) ...))
(define/with-syntax (transform/transformed ...)
(format-temp-ids "~a/transformed" #'(transform ...)))
(define/with-syntax (root-transform/link-request . _)
#'(transform/link-request ...))
(define/with-syntax recursive-call
#'(process-queue pending-requests
processed-requests
transform/transformed ...))
(define/with-syntax (node/extract-link-requests ...)
(format-temp-ids "~a/extract-link-requests" #'(node ...)))
<fold-type-clauses>
<fold-type-stx>
<stx-extract-link-requests>]
To build the graph database, we take the parameters for the root transform, and
return lists incomplete nodes (one for each transform).
The parameters for the root transform, addition to the transform's name, form
the first link request. To fulfil this link request and the ones found later,
we call the desired transform which returns an incomplete node. We extract any
link requests found in that incomplete node, and queue them. The incomplete node
itself is added to the appropriate list, to be returned once the queue has been
fully processed.
@CHUNK[<make-graph-database>
(: make-graph-database
( root-transform/param-type ...
(List (Listof transform/result-node/incomplete) ...)))]
The @tc[make-graph-database] function consists mainly in the process-queue
function, which takes a queue for each transform, and a list of
already-processed incomplete nodes for each transform, and returns these lists,
once all queues are empty.
@CHUNK[<make-graph-database>
(define (make-graph-database root-transform/param ...)
(: process-queue ( (Setof (U transform/link-request ...))
(Setof (U transform/link-request ...))
(Listof transform/result-node/incomplete)
...
(List (Listof transform/result-node/incomplete)
...)))
(define (process-queue pending-requests
processed-requests
transform/transformed
...)
<define-extract-link-requests> ;; TODO: Can probably be moved out.
<process-queue-body>)
<process-queue-initial-call>)]
The @tc[process-queue] function is initially called with empty lists for all
queues and all result lists, except for the root transform's queue, which
contains the initial link request.
@CHUNK[<process-queue-initial-call>
(process-queue (set (root-transform root-transform/param ...))
(set)
(begin 'transform/transformed '())
...)]
Process-queue is a standard queue handler using sets.
@CHUNK[<process-queue-body>
(if (set-empty? pending-requests)
(list transform/transformed ...)
(let* ([request (set-first pending-requests)]
[pending-requests (set-rest pending-requests)]
[processed-requests (set-add processed-requests request)]
[tag (car (transform/link-request-pre-declared-key request))])
<process-queue-body-tags>))]
To process each link request, we first match on its type, and once we found it,
we call the result thunk, extract any link requests contained within, and add
those to the queue.
@CHUNK[<process-queue-body-tags>
(cond
[(eq? tag 'transform)
(let* ([transformed
: transform/result-node/incomplete
(apply transform/link-request→incomplete
(cdr (transform/link-request-pre-declared-key
request)))]
[transform/transformed
(cons transformed transform/transformed)]
[extracted
(list->set
(transform/result-node/extract-link-requests transformed))]
[pending-requests
(set-union pending-requests
(set-subtract extracted processed-requests))])
recursive-call)]
...)]
@subsection[#:tag "graph|TODO3"]{TODO}
We need to traverse the @tc[transformed] node (which is an incomplete node),
and find the link requests within. These link requests will be added to the
corresponding @tc[pending-requests] queue. Below is the body of a for-syntax
function that transforms a type with link-requests into the @tc[match] patterns
that will be used at run-time to traverse the incomplete node. In most cases,
there is only one pattern, but the @tc[U] requires one for each possibility.
When we encounter a link request, we prepend it to the corresponding queue.
For the type @tc[(List Number n/link-request)], the function will look like
this:
@chunk[<fold-type-match-example>
(match transformed
[(list a b)
(match a [a2 a2])
(match b [(and t
(transform/link-request-pre-declared
(cons 'transform1 _)))
(set! pending-requests
(cons t pending-requests))])])]
@subsubsection{Match clauses}
We first transform the type into the different match clauses. For that, we
define the @tc[fold-type-clauses] function, which takes the identifier to
destructure at run-time, and its type. The function returns a list of clauses.
@chunk[<fold-type-clauses>
(define (fold-type-clauses val t)
(syntax-parse t
<fold-type-clauses-body>))]
When a link request is found in the type, we produce the corresponding match
clause, which body prepends the request to the queue of pending requests. For
now we use @racket[set!] to prepend the request, but it would be cleaner to use
recursion. We wouldn't even need to flatten the pending-requests list, because
it could be a tree instead of a flat list, since we only need to add to it and
later pop elements.
TODO: we currently ignore potential hiding of identifiers due to type variables
bound by Rec, for example. This is a case where having a fold-type function
provided by the type-expander library would be interesting.
@CHUNK[<fold-type-clauses-body>
[x:id
#:when (ormap (curry free-identifier=? #'x)
(syntax->list #'(node/incomplete ...)))
(define/with-syntax (this-field-type ...)
(assoc-syntax #'x #'((node/incomplete field-type ...) ...)))
(define/with-syntax (tmp ...)
(generate-temporaries #'(this-field-type ...)))
#`([(x tmp ...)
(append #,@(stx-map fold-type
#'(tmp ...)
#'(this-field-type ...)))])]]
@CHUNK[<fold-type-clauses-body>
[x:id
#:when (ormap (curry free-identifier=? #'x)
(syntax->list #'(node ...)))
#`([(and t (transform/link-request-pre-declared (cons 'transform _)))
(cons (ann t transform/link-request) '())]
...)]]
We handle fixed-length lists by calling @tc[fold-type] on each element type.
@CHUNK[<fold-type-clauses-body>
[((~literal List) a ...)
(define/with-syntax (tmp ...) (generate-temporaries #'(a ...)))
#`([(list tmp ...)
(append #,@(stx-map fold-type #'(tmp ...) #'(a ...)))])]]
We iterate variable-length lists at run-time.
@CHUNK[<fold-type-clauses-body>
[((~literal Listof) a)
#`([(list tmp (... ...))
(append-map (λ (tmp1) #,(fold-type #'tmp1 #'a))
tmp)])]]
Pairs and vectors are handled similarly:
@CHUNK[<fold-type-clauses-body>
[((~literal Pairof) a b)
#`([(cons tmpa tmpb)
(list #,(fold-type #'tmpa #'a)
#,(fold-type #'tmpb #'b))])]]
@CHUNK[<fold-type-clauses-body>
[((~literal Vectorof) a)
#'([(vector tmp (... ...))
(append-map (λ (tmp1) #,(fold-type #'tmp1 #'a))
tmp)])]]
For unions, we return several clauses, obtained via a recursive call to
@tc[fold-type-clauses].
@CHUNK[<fold-type-clauses-body>
[((~literal U) a ...)
#`(#,@(stx-map fold-type-clauses val #'(a ...)))]]
We handle other cases by leaving them as-is, but we still check that they don't
contain a reference to a node type, because we would otherwise leave the
link-request there.
And the fourth maps transforms to the link-requests extraction procedure for
their returned node.
@chunk[<stx-transform/result-node/extract-link-requests>
(define/with-syntax (transform/result-node/extract-link-requests ...)
(for/list ([x (in-syntax #'(result-type ...))])
(assoc-syntax x #'([node . node/extract-link-requests] ...))))]
The last case is when we encounter an unknown type. We assume that it does not
contain any link-requests and therefore return an empty list.
@CHUNK[<fold-type-clauses-body>
[x:id
#`([_ '()])]]
@subsubsection{Folding the type: extracting link requests}
The for-syntax function @tc[fold-type] generates code that uses @tc[match] to
extract the @tc[link-request]s from an incomplete node (or part of it) with type
@tc[t]. The match clauses are those returned by @tc[fold-type-clauses] defined
above.
@CHUNK[<fold-type-stx>
(define (fold-type val t)
#`(begin
(match #,val #,@(fold-type-clauses val t))))]
@subsubsection{Fold function for each incomplete node}
For each node type, we wish to declare a function that extracts link requests
from the incomplete type. We should work on the expanded type.
@chunk[<stx-extract-link-requests>
(define-template-metafunction (fold-type-tmpl stx)
(syntax-case stx () [(_ val t) (fold-type #'val #'t)]))]
@CHUNK[<define-extract-link-requests>
#,@(for/list ([name (in-syntax #'(node/extract-link-requests ...))]
[val-type (in-syntax #'(node/incomplete ...))]
[field-types (in-syntax #'((field-type ...) ...))])
#`(define (#,name [val : #,val-type])
: (Listof (U transform/link-request ...))
#,(fold-type #'val val-type)))]
@subsubsection[#:tag "graph|TODO1"]{TODO}
Later, we will replace link requests with thunks returning the desired node,
wrapped in a promise in order to please occurrence typing. Below is the body of
the for-syntax function that transforms a type with link-requests into a type
with actual nodes. It's probably not useful, because we obtain the same result
with scopes.
@CHUNK[<fold-type-cases>
[x:id
#:when
(ormap (curry free-identifier=? #'x)
(syntax->list #'(node/link-request ...)))
#`(Promise ( #,(assoc-syntax #'x #'((node/link-request . node) ...))))]
[((~literal List) a ...) #`(List #,@(stx-map fold-type #'(a ...)))]
[((~literal Listof) a) #`(Listof #,@(stx-map fold-type #'(a ...)))]
[((~literal Pairof) a b) #`(Pairof #,(fold-type #'a) #,(fold-type #'b))]
[((~literal Vectorof) a) #'(Vectorof #,(fold-type #'a))]
[((~literal U) a ...) #'(U #,(stx-map fold-type #'(a ...)))]]
@section{@racket[incomplete] type-expander}
We define a @tc[type-expander] @tc[(incomplete n)] that returns the incomplete
node type for the node type @tc[n]. This type-expander allows the user to refer
to the incomplete type of the node in the body of a transform, if annotations
are needed for a value containing such a node.
@chunk[<outermost-incomplete>
(define-type-expander (incomplete stx)
(syntax-case stx ()
[(_ n)
(raise-syntax-error
'incomplete
(format "Type doesn't have an incomplete counterpart: ~a"
(syntax->datum #'n))
#'n)]))]
@chunk[<save-outer-incomplete>
(define-type-expander (outer-incomplete stx)
(syntax-case stx () [(_ n) #'(incomplete n)]))]
@chunk[<incomplete>
(let ()
<save-outer-incomplete>
(let ()
(define-type node
(tagged node [field (Promise field-type)] ...))
...
(define-type node/incomplete
;; TODO: substitute link-requests here
(tagged node [field (Promise field-type)] ...))
(define-type-expander (incomplete stx)
(syntax-parse stx ()
[(_ (~litral node)) #'node/incomplete]
[_ #'(outer-incomplete n)]))
<body>))]
@section{Transforming @racket[incomplete] nodes into complete ones}
@subsection{Initial version}
We will start with a very simple traversal function, that will just substitute
link requests immediately in the fields of a node.
@chunk[<substitute-link-requests>
(define (substitute-link-requests v)
(match v
[(node/incomplete field ...)
(node <link-request→promise> ...)]
...))]
@chunk[<link-request→promise>
(match field
[(transform/link-request key _) (transform/key→promise key)] ;; TODO
...)]
@chunk[<transform/key→promise>
]
@subsection{More complex attempt}
We know for sure that all references to future nodes are actually incomplete
ones, but we have no guarantee about the contents of the fields of a node. Since
they may contain a mix of link requests and primitives (via a @tc[U] type for
example), and may contain lists of nodes etc. we need to traverse them at
run-time, in order to find and replace references to link requests.
However, if we were to write this as a simple recursive function, we wouldn't be
able to express its type without knowing anything about the node's type:
@chunk[<attempt-at-typing-traverse>
(case→ ( node/link-request node) ...
( (Pairof may-contain-link-request
may-contain-link-request)
(Pairof doesnt-contain-link-request
doesnt-contain-link-request)))]
Writing the @tc[may-contain-link-request] and @tc[doesnt-contain-link-request]
as functions, while expressing the contraint that the output is the same type as
the input except for the link requests that turned into nodes, would be
impossible in typed/racket. I suppose that with GADTs one could write such a
type.
Instead, we will, during macro-expansion, traverse the type, and generate
conversion procedures accordingly.
@chunk[<fold-type-cases2>
[(~literal node/link-request) #''link-request]
...
[((~literal List) a ...) #'(List #,@(stx-map fold-type #'(a ...)))]
[((~literal Listof) a) #''Listof]
[((~literal Pairof) a) #''Pairof]
[((~literal Vectorof) a) #''Vectorof]
[((~literal U) a ...) #''U]]
@chunk[<traverse-list-type>
( (List a ...) (List replaced-a ...))]
@chunk[<traverse-list-code>
[(list? v) (map traverse-list v)]
[(pair? v) (cons (traverse-list (car v))
(traverse-list (cdr v)))]
[(vector? v) ]]
@subsection{Unions}
Unions are difficult to handle: At one extreme, we confuse two different types
like @tc[(Listof Number)] and @tc[(Listof String)], by using just the @tc[list?]
predicate. On the other end of the spectrum, we try to distinguish them with
@tc[typed/racket]'s @tc[make-predicate], which doesn't work in all cases.
Handling this in the best way possible is out of the scope of this project, so
we will just add special cases as-needed.
@subsection{Unhandled}
We currently don't handle structure types, prefab structures, hash tables,
syntax objects and lots of other types.
On the other hand, we can't handle fixed-length @tc[(Vector ...)] types, because
occurrence typing currently can't track which case we are in when we check the
length with @tc[(vector-length constant)]. We also can't handle functions, for
hopefully obvious reasons.
@; TODO: insert a link to the type-expander document in the paragraph below.
We run into a problem though with types declared via define-type without
informing the type-expander. The type-expander handles these by expanding just
their arguments, and leaving the type untouched, but we can't ignore them in our
case.
For all these other cases, we'll just check that they don't contain any
reference to a link-request type.
@chunk[<fold-type-cases2>
[other
(fold-check-no-link-requests #'other)
#'other]]
The checker below is approximate, and is just meant to catch the error as soon
as possible, and we include a fall-back case for anything we couldn't handle
properly. If we let a link-request slip, it should be caught by the type
checker, unless it is absorbed by a larger type, like in
@tc[(U Any link-request)], in which case it doesn't matter.
@chunk[<fold-check-no-link-requests>
(define (fold-check-no-link-requests stx)
(syntax-parse stx
[(~and whole (~or (~literal node/link-request) ...))
(raise-syntax-error
'graph
"Found a link request buried somewhere I can't access"
whole)]
[(~and whole (t ...))
(stx-map fold-check-no-link-requests #'(t ...))]
[whole whole]))]
@section[#:tag "graph|TODO2"]{TODO}
@chunk[<multiassoc-syntax>
(define (multiassoc-syntax query alist)
(map stx-cdr
(filter (λ (xy) (free-identifier=? query (stx-car xy)))
(syntax->list alist))))
(define (assoc-syntax query alist)
(let ([res (assoc query (map syntax-e (syntax->list alist))
free-identifier=?)])
(unless res (raise-syntax-error '? (format "Can't find ~a in ~a"
query
alist)))
(cdr res)))]
@CHUNK[<old-make-graph-database>
;; The actual traversal code:
;; TODO: write a tail-recursive version, it's cleaner than using set!.
(: make-graph-database
( root-transform.param.type ...
(case→ ( 'node.name (Listof (Pairof Any node.incomplete)))
...)))
(define (make-graph-database root-transform.param.name ...)
(let ([pending : (Listof (U node.link-request ...))
(list (cons (list 'root-transform.name
root-transform.param.name ...)
(λ () (root-transform.function
root-transform.param.name ...))))]
[all-transformed : (Listof (Pairof Symbol Any)) '()]
;; the key is actually the second element in a
;; link-request-???, but should be just a number like in
;; the C# version.
[node.transformed : (Listof (Pairof Any node.incomplete)) '()]
...)
(do : (case→ ( 'node.name (Listof (Pairof Any node.incomplete)))
...)
()
[(null? pending)
(ann (λ (selector)
(cond [(eq? selector 'node.name) node.transformed] ...))
(case→ ( 'node.name (Listof (Pairof Any node.incomplete)))
...))]
(let ((request (car pending)))
;; Must be immediately after the (let (...), because we cons to
;; that list in the block below.
(set! pending (cdr pending))
;; Skip already-transformed link requests. TODO: map a number
;; for each.
(unless (member (car request) all-transformed)
;; Call the lambda-part of the request.
(let ([transformed ((cdr request))])
(cond
[(eq? (car transformed) 'node.name)
(set! pending
(list* ((cdr transformed)
'node/field-filter-out-primitives/name)
...
pending))
(set! all-transformed (cons (car request)
all-transformed))
(set! node.transformed
(cons (cons (car request) (cdr transformed))
node.transformed))]
...
;; Make sure all cases are treated, at compile-time.
[else (typecheck-fail #'#,stx
"incomplete coverage")])))))))]
@section{Tests}
@chunk[<test-graph>
(values)]
@section{Conclusion}
@chunk[<*>
(begin
(module main typed/racket
(require (for-syntax racket/sequence
;; in-syntax on older versions
;;;unstable/sequence
syntax/parse
syntax/parse/experimental/template
racket/syntax
racket/function
syntax/stx
racket/pretty
"../lib/low-untyped.rkt"
"../lib/untyped.rkt")
(prefix-in DEBUG-tr: typed/racket)
syntax/parse
"../lib/low.rkt"
"structure.lp2.rkt"
"variant.lp2.rkt"
"../type-expander/multi-id.lp2.rkt"
"../type-expander/type-expander.lp2.rkt")
(provide make-graph-constructor
#|graph|#)
(begin-for-syntax
<multiassoc-syntax>)
<pre-declare-transform/link-request>
<make-graph-constructor>
#|<graph>|#)
(require 'main)
(provide (all-from-out 'main))
(module* test typed/racket
(require (submod "..")
"../type-expander/type-expander.lp2.rkt"
"../lib/test-framework.rkt")
;; Debug
<pre-declare-transform/link-request>
(require syntax/parse
"../lib/low.rkt"
"structure.lp2.rkt"
"variant.lp2.rkt"
"../type-expander/multi-id.lp2.rkt"
"../type-expander/type-expander.lp2.rkt")
;;
<test-graph>
<test-make-graph-constructor>))]

View File

@ -1,206 +1,214 @@
#lang debug typed/racket
#lang typed/racket
(require (for-syntax racket/syntax
racket/function
syntax/stx
syntax/parse
syntax/parse/experimental/template
"../lib/low-untyped.rkt")
(for-meta 2
racket/base
racket/syntax)
"../lib/low.rkt"
"map1.rkt"
"graph4.lp2.rkt"
"get.lp2.rkt"
"../type-expander/type-expander.lp2.rkt")
(provide map: apply-compose) ;; TODO: move apply-compose to lib/low.rkt
(module m typed/racket
(provide car! cdr!)
(: car! ( (A) ( (U (Listof A) (Pairof A Any)) A)))
(define (car! x) (if (pair? x)
(car x)
(car x)))
(: cdr! ( (A) (case→ ( (Listof A) (Listof A))
( (Pairof Any A) A))))
(define (cdr! x) (cdr x)))
(begin-for-syntax
(define-syntax-class lam
(pattern (~or (~literal λ) (~literal lambda))))
(define-syntax-class mapp
(pattern (~or (~literal map) (~literal map:)))))
(require 'm)
(provide (all-from-out 'm))
(define-for-syntax (remove-identities stx)
(provide map: compose-maps)
(define-syntax (dbg stx)
(syntax-parse stx
[() #'()]
[((~or (~lit identity) (~lit values) (~lit compose)) . rest)
(remove-identities #'rest)]
[([(~literal compose) . fs] . rest)
(define/with-syntax cleaned-fs (remove-identities #'fs))
(syntax-parse #'cleaned-fs
[() (remove-identities #'rest)]
[(one-f) #`(one-f . #,(remove-identities #'rest))]
[some-fs #`((compose . some-fs) . #,(remove-identities #'rest))])]
[(f . rest)
#`(f . #,(remove-identities #'rest))]))
;; TODO: check that we don't bork the literals identity, values and compose
;; inside macros or function calls, or alter them in any other way, e.g.
;; (map: (compose identity (λ (values) (+ values 1)) identity) '(1 2 3))
;; or
;; (define (calltwice f) (λ (x) (f (f x))))
;; (map: (compose (calltwice identity)) '(1 2 3))
;; Although a poor variable name choice, the two occurences of "values" in the
;; first example shouldn't be altered, and the λ itself shouldn't be touched.
;; In the second one, everything inside the calltwice function call should be
;; left intact.
(define-for-syntax (remove-identities1 stx)
(syntax-parse (remove-identities #`(#,stx))
[() #'identity]
[(f) #'f]))
[(_ (~optional (~and norun #:norun)) code)
(if (attribute norun)
#'(ann 'code Any)
#'code)]))
(begin-for-syntax
(define-syntax-class map-info
(pattern (_ #:in in-type
#:in-∀ [in-∀ ]
#:out out-type
#:∀ (∀-type )
#:arg-funs ([arg-fun
param-fun
(~optional (~and auto-in #:auto-in))
fun-in fun-out] )
#:funs [fun ]))))
(define-syntax-class >0 (pattern :exact-positive-integer)))
(begin-for-syntax
(define-syntax (mk-info stx)
(syntax-case stx ()
[(_ . rest-stx)
(begin
(define/with-syntax whole-stx (syntax/loc stx (info . rest-stx)))
#'(syntax-parse #`whole-stx
[i:map-info #'i]))])))
(define-syntax-class ≥0 (pattern :exact-nonnegative-integer)))
(define-for-syntax (:map* stx* stx-&ls stx-out stx-out-∀)
(if (stx-null? stx*)
'()
(syntax-parse (:map (stx-car stx*) stx-&ls stx-out stx-out-∀)
[info:map-info
(let ([r (:map* (stx-cdr stx*) stx-&ls #'info.in-type #'[info.in-∀ ])]
[auto (attribute info.auto-in)])
(if (and (not (null? auto)) (car auto) (not (null? r)))
(syntax-parse (car r)
[r-info:map-info
(let ([intact #'([info.arg-fun
info.param-fun
info.fun-in ;;;
info.fun-out] )]
[replaced #'([info.arg-fun
info.param-fun
r-info.out-type ;;info.fun-in ;;;
info.fun-out] )])
(cons (mk-info #:in info.in-type
#:in-∀ [info.in-∀ ]
#:out info.out-type
#:∀ (info.∀-type )
#:arg-funs (#,(stx-car replaced)
#,@(stx-cdr intact))
#:funs [info.fun ])
r))])
(cons #'info r)))])))
(define-for-syntax (:map stx stx-&ls stx-out stx-out-∀)
(define/with-syntax (&l ) stx-&ls)
(define/with-syntax out stx-out)
(define/with-syntax (out-∀ ) stx-out-∀)
(syntax-parse (remove-identities1 stx)
[(~literal car)
#'(info #:in (Pairof out Any) #:in-∀ [] #:out out #:∀ []
#:arg-funs [] #:funs [car])]
[(~literal cdr)
#'(info #:in (Pairof Any out) #:in-∀ [] #:out out #:∀ []
#:arg-funs [] #:funs [cdr])]
;; TODO: should remove `identity` completely, doing (map identity l) is
;; useless appart for constraining the type, but it's an ugly way to do so.
[(~literal identity)
#'(info #:in out #:in-∀ [] #:out out #:∀ []
#:arg-funs [] #:funs [identity])]
[((~literal compose) f )
(syntax-parse (:map* #'(f ) #'(&l ) #'out #'[out-∀ ])
[(~and (_ rightmost:map-info) (leftmost:map-info . _) (:map-info ))
#'(info #:in rightmost.in-type
#:in-∀ [rightmost.in-∀ ];; ??
#:out leftmost.out-type
#:∀ [∀-type ]
#:arg-funs [(arg-fun param-fun fun-in fun-out) ]
#:funs [fun ])])]
[((~literal curry) :mapp f)
(syntax-parse (internal-map: #'f #'(&l ) #'out #'[out-∀ ])
[(i:map-info . code)
#`(info #:in (Listof i.in-type)
#:in-∀ [i.in-∀ ];; ??
#:out (Listof out)
#:∀ [i.∀-type ]; i.out-type
#:arg-funs [(i.arg-fun i.param-fun i.fun-in i.fun-out) ]
#:funs [#,(syntax/loc #'f (code i.fun _))])])]
[(~literal length)
(define-temp-ids "&~a" f)
(define-temp-ids "~a/in" f)
#'(info #:in f/in #:in-∀ [f/in] #:out out #:∀ [f/in] ;;??
#:arg-funs [((λ ([l : (Listof Any)]) (length l))
&f
#:auto-in f/in
out)]
#:funs [&f])]
[((~literal λget) pat )
#'(info #:in (has-get out pat )
#:in-∀ [out-∀ ]
#:out (result-get out pat )
#:∀ []
#:arg-funs []
#:funs [(get _ pat )])]
[f
(define-temp-ids "&~a" f)
(define-temp-ids "~a/in" f)
#'(info #:in f/in #:in-∀ [f/in] #:out out #:∀ [f/in] ;;??
#:arg-funs [(f &f #:auto-in f/in out)] #:funs [&f])]))
(define-syntax (apply-compose stx)
(define-type-expander (Deep-Listof stx)
(syntax-parse stx
[(_ [] [a ])
#'(values a )]
[(_ [f (~and loc (f-last x (~literal _) y ))] [a ])
#`(apply-compose [f ] [#,(syntax/loc #'loc (f-last x a y ))])]
[(_ [f f-last] [a ])
#`(apply-compose [f ] [#,(syntax/loc #'f-last (f-last a ))])]))
[(_ 0 T)
#'T]
[(_ d:>0 T)
#`(Listof (Deep-Listof #,(sub1 (syntax-e #'d)) T))]))
(define-for-syntax (internal-map: stx-f stx-&ls stx-out stx-out-∀)
(define/with-syntax f stx-f)
(define/with-syntax (&l ) stx-&ls)
(define/with-syntax out stx-out)
(define/with-syntax (out-∀ ) stx-out-∀)
(syntax-parse (:map #'f #'(&l ) #'out #'[out-∀ ])
[(~and i :map-info)
(define/with-syntax map1 (generate-temporary #'map))
(cons #'i
(template
(define-syntax (λdeep-map stx)
(syntax-parse stx
[(_ {∀-type:id } A:expr B:expr 0)
#'(ann (λ (f x) (f x))
( (∀-type )
( ( A B) A B)
;; Use the type below to allow identity functions, but it's more
;; heavy on the typechecker
#;(case→ ( ( A B) A B)
( ( A A) A A))))]
[(_ (~and norun #:norun) {∀-type:id } A:expr B:expr d:≥0)
(define/with-syntax local-map (generate-temporary #'map))
#`(dbg norun
(let ()
(: map1 ( [out-∀ ∀-type ]
( ( fun-in fun-out)
(Listof in-type)
(Listof out-type))))
(define (map1 param-fun &l )
(if (or (null? &l) )
(: local-map
( (∀-type )
( ( A B) (Deep-Listof d A) (Deep-Listof d B))
;; Use the type below to allow identity functions, but it's
;; more heavy on the typechecker
#;(case→ ( ( A B) (Deep-Listof d A) (Deep-Listof d B))
( ( A A) (Deep-Listof d A) (Deep-Listof d A)))))
(define (local-map f l)
(if (null? l)
'()
(cons (apply-compose [fun ] [(car &l) ])
(map1 param-fun (cdr &l) ))))
map1)))]));(map1 arg-fun … . ls)
(cons ((λdeep-map {∀-type } A B #,(sub1 (syntax-e #'d)))
f (car l))
(local-map f (cdr l)))))
local-map))]))
(module+ test
(check-equal?: ((λdeep-map {A B} A B 3) add1 '([{1} {2 3}] [{4}]))
: (Listof (Listof (Listof Number)))
'([{2} {3 4}] [{5}])))
(define-syntax (deep-map stx)
(syntax-parse stx
[(_ {∀-type:id } A:expr B:expr d:≥0 f:expr l:expr)
(syntax/loc #'f ((λdeep-map {∀-type } A B d) f l))]))
(module+ test
(check-equal?: (deep-map {A B} A B 3 add1 '([{1} {2 3}] [{4}]))
: (Listof (Listof (Listof Number)))
'([{2} {3 4}] [{5}])))
(module+ test
(check-equal?: (deep-map {A B} A B 0 add1 '7)
: Number
8))
;; We provide hints for the types of some common functions
(define-type-expander (ArgOf stx)
(syntax-parse stx
[(_ (~literal length) T:expr R) #'(Listof Any)]
[(_ (~literal car) T:expr R) #'(Pairof T Any)]
[(_ (~literal car!) T:expr R) #'(U (Listof T) (Pairof T Any))]
[(_ (~literal cdr) T:expr R) #'(Pairof Any T)]
[(_ (~literal list) T:expr R) #'T]
[(_ ((~literal λget) f ) T:expr R) #'(has-get T f )]
;; Default case:
[(_ f:expr T:expr R) #'T]))
(define-type-expander (ResultOf stx)
(syntax-parse stx
[(_ (~literal length) T:expr R) #'Index]
[(_ (~literal car) T:expr R) #'T]
[(_ (~literal car!) T:expr R) #'T]
[(_ (~literal cdr) T:expr R) #'T]
[(_ (~literal list) T:expr R) #'(List T)]
[(_ ((~literal λget) f ) T:expr R) #'(result-get T f )]
;; Default case:
[(_ f:expr T:expr R) #'R]))
(define-syntax (substitute-function stx)
(syntax-parse stx
[(_ (~literal list)) #'(λ #:∀ (X) ([x : X]) : (List X) (list x))]
;; Default case:
[(_ f:expr) #'f]))
(define-syntax/parse (deep-map-auto d:≥0 f l)
#'(deep-map {A B} (ArgOf f A B) (ResultOf f A B) d (substitute-function f) l))
(module+ test
(check-equal?: (deep-map-auto 2 length '([{1} {2 3}] [{4}]))
: (Listof (Listof Index))
'([1 2] [1])))
(module+ test
(check-equal?: (deep-map-auto 2 car '([{1} {2 3}] [{4}]))
: (Listof (Listof Number))
'([1 2] [4])))
(module+ test
(check-equal?: (deep-map-auto 2 list '([1 2] [3]))
: (Listof (Listof (Listof Number)))
'([{1} {2}] [{3}])))
#;(module+ test
(check-equal?: (deep-map-auto 3 add1 (deep-map-auto 2 list '([1 2] [3])))
: (Listof (Listof (Listof Number)))
'([{1} {2}] [{3}])))
(module+ test
(check-equal?: (deep-map-auto 1 length
(deep-map-auto 2 car
(deep-map-auto 2 list
'([1 2] [3]))))
: (Listof Index)
'(2 1)))
;; Now we turn all map: calls into the form
;; (compose-maps [(d f) …] [l …])
(define-syntax (compose-maps stx)
(syntax-parse stx
[(_ [] [l])
#'l]
[(_ [] [l:expr ])
#'(values l )]
[(_ [(d:≥0 f:expr) (d-rest:≥0 f-rest:expr) ] [l:expr ])
#'(deep-map-auto d f (compose-maps [(d-rest f-rest) ] [l ]))]))
(module+ test
(check-equal?: (compose-maps [(2 car!) (3 add1) (3 add1) (2 list)]
['([1 2] [3])])
: (Listof (Listof Number))
'([3 4] [5])))
(define-for-syntax (transform-map: depth stx)
(syntax-parse stx
[((~literal curry) (~literal map) f:expr)
(transform-map: (add1 depth) #'f)]
[((~literal compose) f:expr )
(define/syntax-parse (([dd ff] ) )
(stx-map (curry transform-map: depth) #'(f )))
#`[(dd ff) ]]
[(~literal identity) #'[]]
[(~literal values) #'[]]
[f:expr
#`[(#,depth f)]]))
;; TODO: inefficient at compile-time: we run (:map #'f #'Out) twice.
;; Plus it could cause some bugs because of differing #'Out.
(define-syntax (map: stx)
(syntax-parse stx
[(_ (~optional (~and norun (~literal norun))) f l )
(define-temp-ids "&~a" (l ))
(syntax-parse (internal-map: #'f #'(&l ) #'Out #'[Out])
[(:map-info . code)
(if (attribute norun)
#'(ann '(code arg-fun l ) Any)
#'(code arg-fun l ))])]))
[(_ f l) #`(compose-maps #,(transform-map: 1 #'f) [l])]))
(module* test typed/racket
(require (submod "..")
"../lib/low.rkt")
(module+ test
(check-equal?: (map: car '((1 a) (2 b) (3 c)))
: (Listof Number)
'(1 2 3)))
(module+ test
(check-equal?: (map: ( ( add1)
length
(curry map car)
(curry map list)
(curry map ()))
'([1 2] [3]))
: (Listof Number)
'(3 2)))
(module+ test
;(require (submod "..")
; "../lib/low.rkt")
(check-equal?: (map: add1 '(1 2 3))
: (Listof Number)
@ -262,58 +270,59 @@
: (Listof (Listof Number))
'((2) (3) (4)))
;; The tests below using (curry map: …) don't work, because typed/racket wraps
;; the map: identifier with a contract, so the identifier seen outside the
;; module is not the same as the one used in the syntax-parse ~literal clause.
#|(begin
(check-equal?: (map: (curry map add1) '((1 2 3) (4 5)))
(begin
;; Some of the tests below use (curry map: …), and don't work, because
;; typed/racket wraps the map: identifier with a contract, so the identifier
;; seen outside the module is not the same as the one used in the
;; syntax-parse ~literal clause.
(check-equal?: (map: (curry map add1) '((1 2 3) (4 5)))
: (Listof (Listof Number))
'((2 3 4) (5 6)))
#;(check-equal?: (map: (curry map: add1) '((1 2 3) (4 5)))
: (Listof (Listof Number))
'((2 3 4) (5 6)))
#;(check-equal?: (map: (curry map: add1) '((1 2 3) (4 5)))
: (Listof (Listof Number))
'((2 3 4) (5 6)))
(check-equal?: (map: (curry map (compose number->string add1))
(check-equal?: (map: (curry map (compose number->string add1))
'((1 2 3) (4 5)))
: (Listof (Listof String))
'(("2" "3" "4") ("5" "6")))
#;(check-equal?: (map: (curry map: (compose number->string add1))
'((1 2 3) (4 5)))
: (Listof (Listof String))
'(("2" "3" "4") ("5" "6")))
#;(check-equal?: (map: (curry map: (compose number->string add1))
'((1 2 3) (4 5)))
: (Listof (Listof String))
'(("2" "3" "4") ("5" "6")))
(check-equal?: (map: add1 '(1 2 3))
: (Listof Number)
'(2 3 4))
(check-equal?: (map: car '((1 a) (2 b) (3 c)))
: (Listof Number)
'(1 2 3))
(check-equal?: (map: (curry map car) '([{1 a} {2 b}] [{3 c}]))
(check-equal?: (map: add1 '(1 2 3))
: (Listof Number)
'(2 3 4))
(check-equal?: (map: car '((1 a) (2 b) (3 c)))
: (Listof Number)
'(1 2 3))
(check-equal?: (map: (curry map car) '([{1 a} {2 b}] [{3 c}]))
: (Listof (Listof Number))
'([1 2] [3]))
#;(check-equal?: (map: (curry map: car) '([{1 a} {2 b}] [{3 c}]))
: (Listof (Listof Number))
'([1 2] [3]))
#;(check-equal?: (map: (curry map: car) '([{1 a} {2 b}] [{3 c}]))
: (Listof (Listof Number))
'([1 2] [3]))
(check-equal?: (map: (curry map (curry map car))
(check-equal?: (map: (curry map (curry map car))
'([((1 a) (2 b)) ((3 c))] [((4))]))
: (Listof (Listof (Listof Number)))
'([(1 2) (3)] [(4)]))
#;(check-equal?: (map: (curry map (curry map: car))
'([((1 a) (2 b)) ((3 c))] [((4))]))
: (Listof (Listof (Listof Number)))
'([(1 2) (3)] [(4)]))
#;(check-equal?: (map: (curry map (curry map: car))
'([((1 a) (2 b)) ((3 c))] [((4))]))
: (Listof (Listof (Listof Number)))
'([(1 2) (3)] [(4)]))
#;(check-equal?: (map: (curry map: (curry map car))
'([((1 a) (2 b)) ((3 c))] [((4))]))
: (Listof (Listof (Listof Number)))
'([(1 2) (3)] [(4)]))
#;(check-equal?: (map: (curry map: (curry map: car))
'([((1 a) (2 b)) ((3 c))] [((4))]))
: (Listof (Listof (Listof Number)))
'([(1 2) (3)] [(4)])))|#
#;(check-equal?: (map: (curry map: (curry map car))
'([((1 a) (2 b)) ((3 c))] [((4))]))
: (Listof (Listof (Listof Number)))
'([(1 2) (3)] [(4)]))
#;(check-equal?: (map: (curry map: (curry map: car))
'([((1 a) (2 b)) ((3 c))] [((4))]))
: (Listof (Listof (Listof Number)))
'([(1 2) (3)] [(4)])))
(check-equal?: (map: car '((1 b x) (2 c) (3 d)))
: (Listof Number)
@ -342,117 +351,4 @@
#|
(check-equal?: (map: + '(1 2 3) '(4 5 6))
: (Listof Number)
'(5 7 9))|#)
#|
(map: (compose F (curry map add1)) '((1 2) (3)))
Problem: in the code above, the input type of `F` has to be the return type of
`(curry map add1)`, i.e. `(Listof B)`. The return type of `F` may depend on its
input type (e.g. wrapping a value), so the type information flows leftwards
inside `compose`.
However, if F is a destructuring operation, like `car` or `cdr`, it may impose
constraints on the return type of the function immediately to its right, meaning
that the type information flows rightwards.
It seems difficult to reconcile these two cases without writing a complex
algorithm.
Worst-case scenario:
+-- constrains to the right
v v-- constrains to the right
(compose car complex-calculation (curry map car))
^ ^-- gives a (Listof ?) to the left
+-- constrained on both sides
Maybe we could cover most common cases by first getting the type for the handled
cases which impose constraints to the right and/or give a type to the left, and
then use these types instead of the , to fill in the holes for other functions.
EDIT: that's what we did, using the #:auto-in
|#
#|
(define-for-syntax (map-infer-types stx)
(syntax-parse stx
[(_ (~literal car))
(values #'(A B)
#'(Pairof A B))]
[(_ (~literal cdr)) #'(Pairof Any T)]
[(_ T (~literal values)) #'T]
[(_ T ((~literal compose))) #'T]
[(_ T ((~literal compose) f0 . fs))
#'(map-element (map-element T f0) (compose . fs))]
[(_ T ((~literal curry) (~or (~literal map:) (~literal map)) f) l)
#''_]
;; get
[(_ f . ls)
;; TODO:
#'T]))
(define-type-expander (map-element stx)
(syntax-parse stx
[(_ T:id (~literal car)) #'(Pairof T Any)]
[(_ T:id (~literal cdr)) #'(Pairof Any T)]
[(_ T (~literal values)) #'T]
[(_ T ((~literal compose))) #'T]
[(_ T ((~literal compose) f0 . fs))
#'(map-element (map-element T f0) (compose . fs))]
[(_ T ((~literal curry) (~or (~literal map:) (~literal map)) f) l)
#''_]
;; get
[(_ f . ls)
;; TODO:
#'T]))
(define-type-expander (map-result stx)
(syntax-parse stx
[(_ T:id (~literal car)) #'T]
[(_ T:id (~literal cdr)) #'T]))
(define-syntax (map: stx)
(syntax-parse stx
[(_ (~literal car) l) #'((curry-map A A (Pairof A Any) car) l)]
[(_ (~literal cdr) l) #'((curry-map B B (Pairof Any B) cdr) l)]
;; TODO: add caar etc.
[(_ ((~literal values)) l) #'l]
[(_ ((~literal compose)) l) #'l]
[(_ ((~literal compose) f0 . fs) l) #'(map: f0 (map: (compose . fs) l))]
[(_ ((~literal curry) (~or (~literal map:) (~literal map)) f) l)
#''_]
[(_ ((~literal λget) field-or-accessor ) l)
#'(get l ( ) field-or-accessor )]
[(_ f . ls)
#'(map f . ls)]))
|#
;|#
'(5 7 9))|#)

View File

@ -6,7 +6,7 @@
syntax/parse/experimental/template
"../lib/low-untyped.rkt")
"../lib/low.rkt"
"graph4.lp2.rkt"
"get.lp2.rkt"
"../type-expander/type-expander.lp2.rkt")
;; TODO: DEBUG

View File

@ -1,354 +0,0 @@
#lang typed/racket
(require (for-syntax racket/syntax
racket/function
syntax/stx
syntax/parse
"../lib/low-untyped.rkt")
"../lib/low.rkt"
"graph4.lp2.rkt"
"../type-expander/type-expander.lp2.rkt")
(module m typed/racket
(provide car! cdr!)
(: car! ( (A) ( (U (Listof A) (Pairof A Any)) A)))
(define (car! x) (if (pair? x)
(car x)
(car x)))
(: cdr! ( (A) (case→ ( (Listof A) (Listof A))
( (Pairof Any A) A))))
(define (cdr! x) (cdr x)))
(require 'm)
(provide (all-from-out 'm))
(provide map: compose-maps)
(define-syntax (dbg stx)
(syntax-parse stx
[(_ (~optional (~and norun #:norun)) code)
(if (attribute norun)
#'(ann 'code Any)
#'code)]))
(begin-for-syntax
(define-syntax-class >0 (pattern :exact-positive-integer)))
(begin-for-syntax
(define-syntax-class ≥0 (pattern :exact-nonnegative-integer)))
(define-type-expander (Deep-Listof stx)
(syntax-parse stx
[(_ 0 T)
#'T]
[(_ d:>0 T)
#`(Listof (Deep-Listof #,(sub1 (syntax-e #'d)) T))]))
(define-syntax (λdeep-map stx)
(syntax-parse stx
[(_ {∀-type:id } A:expr B:expr 0)
#'(ann (λ (f x) (f x))
( (∀-type )
( ( A B) A B)
;; Use the type below to allow identity functions, but it's more
;; heavy on the typechecker
#;(case→ ( ( A B) A B)
( ( A A) A A))))]
[(_ (~and norun #:norun) {∀-type:id } A:expr B:expr d:≥0)
(define/with-syntax local-map (generate-temporary #'map))
#`(dbg norun
(let ()
(: local-map
( (∀-type )
( ( A B) (Deep-Listof d A) (Deep-Listof d B))
;; Use the type below to allow identity functions, but it's
;; more heavy on the typechecker
#;(case→ ( ( A B) (Deep-Listof d A) (Deep-Listof d B))
( ( A A) (Deep-Listof d A) (Deep-Listof d A)))))
(define (local-map f l)
(if (null? l)
'()
(cons ((λdeep-map {∀-type } A B #,(sub1 (syntax-e #'d)))
f (car l))
(local-map f (cdr l)))))
local-map))]))
(module+ test
(check-equal?: ((λdeep-map {A B} A B 3) add1 '([{1} {2 3}] [{4}]))
: (Listof (Listof (Listof Number)))
'([{2} {3 4}] [{5}])))
(define-syntax (deep-map stx)
(syntax-parse stx
[(_ {∀-type:id } A:expr B:expr d:≥0 f:expr l:expr)
(syntax/loc #'f ((λdeep-map {∀-type } A B d) f l))]))
(module+ test
(check-equal?: (deep-map {A B} A B 3 add1 '([{1} {2 3}] [{4}]))
: (Listof (Listof (Listof Number)))
'([{2} {3 4}] [{5}])))
(module+ test
(check-equal?: (deep-map {A B} A B 0 add1 '7)
: Number
8))
;; We provide hints for the types of some common functions
(define-type-expander (ArgOf stx)
(syntax-parse stx
[(_ (~literal length) T:expr R) #'(Listof Any)]
[(_ (~literal car) T:expr R) #'(Pairof T Any)]
[(_ (~literal car!) T:expr R) #'(U (Listof T) (Pairof T Any))]
[(_ (~literal cdr) T:expr R) #'(Pairof Any T)]
[(_ (~literal list) T:expr R) #'T]
[(_ ((~literal λget) f ) T:expr R) #'(has-get T f )]
;; Default case:
[(_ f:expr T:expr R) #'T]))
(define-type-expander (ResultOf stx)
(syntax-parse stx
[(_ (~literal length) T:expr R) #'Index]
[(_ (~literal car) T:expr R) #'T]
[(_ (~literal car!) T:expr R) #'T]
[(_ (~literal cdr) T:expr R) #'T]
[(_ (~literal list) T:expr R) #'(List T)]
[(_ ((~literal λget) f ) T:expr R) #'(result-get T f )]
;; Default case:
[(_ f:expr T:expr R) #'R]))
(define-syntax (substitute-function stx)
(syntax-parse stx
[(_ (~literal list)) #'(λ #:∀ (X) ([x : X]) : (List X) (list x))]
;; Default case:
[(_ f:expr) #'f]))
(define-syntax/parse (deep-map-auto d:≥0 f l)
#'(deep-map {A B} (ArgOf f A B) (ResultOf f A B) d (substitute-function f) l))
(module+ test
(check-equal?: (deep-map-auto 2 length '([{1} {2 3}] [{4}]))
: (Listof (Listof Index))
'([1 2] [1])))
(module+ test
(check-equal?: (deep-map-auto 2 car '([{1} {2 3}] [{4}]))
: (Listof (Listof Number))
'([1 2] [4])))
(module+ test
(check-equal?: (deep-map-auto 2 list '([1 2] [3]))
: (Listof (Listof (Listof Number)))
'([{1} {2}] [{3}])))
#;(module+ test
(check-equal?: (deep-map-auto 3 add1 (deep-map-auto 2 list '([1 2] [3])))
: (Listof (Listof (Listof Number)))
'([{1} {2}] [{3}])))
(module+ test
(check-equal?: (deep-map-auto 1 length
(deep-map-auto 2 car
(deep-map-auto 2 list
'([1 2] [3]))))
: (Listof Index)
'(2 1)))
;; Now we turn all map: calls into the form
;; (compose-maps [(d f) …] [l …])
(define-syntax (compose-maps stx)
(syntax-parse stx
[(_ [] [l])
#'l]
[(_ [] [l:expr ])
#'(values l )]
[(_ [(d:≥0 f:expr) (d-rest:≥0 f-rest:expr) ] [l:expr ])
#'(deep-map-auto d f (compose-maps [(d-rest f-rest) ] [l ]))]))
(module+ test
(check-equal?: (compose-maps [(2 car!) (3 add1) (3 add1) (2 list)]
['([1 2] [3])])
: (Listof (Listof Number))
'([3 4] [5])))
(define-for-syntax (transform-map: depth stx)
(syntax-parse stx
[((~literal curry) (~literal map) f:expr)
(transform-map: (add1 depth) #'f)]
[((~literal compose) f:expr )
(define/syntax-parse (([dd ff] ) )
(stx-map (curry transform-map: depth) #'(f )))
#`[(dd ff) ]]
[(~literal identity) #'[]]
[(~literal values) #'[]]
[f:expr
#`[(#,depth f)]]))
(define-syntax (map: stx)
(syntax-parse stx
[(_ f l) #`(compose-maps #,(transform-map: 1 #'f) [l])]))
(module+ test
(check-equal?: (map: car '((1 a) (2 b) (3 c)))
: (Listof Number)
'(1 2 3)))
(module+ test
(check-equal?: (map: ( ( add1)
length
(curry map car)
(curry map list)
(curry map ()))
'([1 2] [3]))
: (Listof Number)
'(3 2)))
(module+ test
;(require (submod "..")
; "../lib/low.rkt")
(check-equal?: (map: add1 '(1 2 3))
: (Listof Number)
'(2 3 4))
(check-equal?: (map: (compose add1) '(1 2 3))
: (Listof Number)
'(2 3 4))
(check-equal?: (map: ( identity add1) '(1 2 3))
: (Listof Number)
'(2 3 4))
(check-equal?: (map: ( add1 identity) '(1 2 3))
: (Listof Number)
'(2 3 4))
(check-equal?: (map: ( number->string add1) '(1 2 9))
: (Listof String)
'("2" "3" "10"))
(check-equal?: (map: ( string-length number->string add1) '(1 2 9))
: (Listof Number)
'(1 1 2))
(check-equal?: (map: car '((1 2) (2) (9 10 11)))
: (Listof Number)
'(1 2 9))
(check-equal?: (map: ( add1 car) '((1 2) (2) (9 10 11)))
: (Listof Number)
'(2 3 10))
(check-equal?: (map: ( string-length number->string add1 car cdr)
'((1 2) (2 3) (8 9 10)))
: (Listof Number)
'(1 1 2))
(check-equal?: (map: identity '(1 2 3))
: (Listof Number)
'(1 2 3))
(check-equal?: (map: values '(1 2 3))
: (Listof Number)
'(1 2 3))
(check-equal?: (map: (compose) '(1 2 3))
: (Listof Number)
'(1 2 3))
(check-equal?: (map: (compose identity) '(1 2 3))
: (Listof Number)
'(1 2 3))
(check-equal?: (map: ( identity values identity values) '(1 2 3))
: (Listof Number)
'(1 2 3))
(check-equal?: (map: ( length (curry map add1)) '((1 2) (3)))
: (Listof Number)
'(2 1))
(check-equal?: (map: (curry map add1) '((1 2) (3)))
: (Listof (Listof Number))
'((2 3) (4)))
(define (numlist [x : Number]) (list x))
(check-equal?: (map: ( (curry map add1) numlist) '(1 2 3))
: (Listof (Listof Number))
'((2) (3) (4)))
(check-equal?: (map: ( (curry map add1) (λ ([x : Number]) (list x)))
'(1 2 3))
: (Listof (Listof Number))
'((2) (3) (4)))
(begin
;; Some of the tests below use (curry map: …), and don't work, because
;; typed/racket wraps the map: identifier with a contract, so the identifier
;; seen outside the module is not the same as the one used in the
;; syntax-parse ~literal clause.
(check-equal?: (map: (curry map add1) '((1 2 3) (4 5)))
: (Listof (Listof Number))
'((2 3 4) (5 6)))
#;(check-equal?: (map: (curry map: add1) '((1 2 3) (4 5)))
: (Listof (Listof Number))
'((2 3 4) (5 6)))
(check-equal?: (map: (curry map (compose number->string add1))
'((1 2 3) (4 5)))
: (Listof (Listof String))
'(("2" "3" "4") ("5" "6")))
#;(check-equal?: (map: (curry map: (compose number->string add1))
'((1 2 3) (4 5)))
: (Listof (Listof String))
'(("2" "3" "4") ("5" "6")))
(check-equal?: (map: add1 '(1 2 3))
: (Listof Number)
'(2 3 4))
(check-equal?: (map: car '((1 a) (2 b) (3 c)))
: (Listof Number)
'(1 2 3))
(check-equal?: (map: (curry map car) '([{1 a} {2 b}] [{3 c}]))
: (Listof (Listof Number))
'([1 2] [3]))
#;(check-equal?: (map: (curry map: car) '([{1 a} {2 b}] [{3 c}]))
: (Listof (Listof Number))
'([1 2] [3]))
(check-equal?: (map: (curry map (curry map car))
'([((1 a) (2 b)) ((3 c))] [((4))]))
: (Listof (Listof (Listof Number)))
'([(1 2) (3)] [(4)]))
#;(check-equal?: (map: (curry map (curry map: car))
'([((1 a) (2 b)) ((3 c))] [((4))]))
: (Listof (Listof (Listof Number)))
'([(1 2) (3)] [(4)]))
#;(check-equal?: (map: (curry map: (curry map car))
'([((1 a) (2 b)) ((3 c))] [((4))]))
: (Listof (Listof (Listof Number)))
'([(1 2) (3)] [(4)]))
#;(check-equal?: (map: (curry map: (curry map: car))
'([((1 a) (2 b)) ((3 c))] [((4))]))
: (Listof (Listof (Listof Number)))
'([(1 2) (3)] [(4)])))
(check-equal?: (map: car '((1 b x) (2 c) (3 d)))
: (Listof Number)
'(1 2 3))
(check-equal?: (map: cdr '((1 b x) (2 c) (3 d)))
: (Listof (Listof Symbol))
'((b x) (c) (d)))
(check-equal?: (map: car (map: cdr '((1 b x) (2 c) (3 d))))
: (Listof Symbol)
'(b c d))
(check-equal?: (map: (compose) '((1 b x) (2 c) (3 d)))
: (Listof (Listof (U Number Symbol)))
'((1 b x) (2 c) (3 d)))
(check-equal?: (map: (compose car) '((1 b x) (2 c) (3 d)))
: (Listof Number)
'(1 2 3))
(check-equal?: (map: (compose cdr) '((1 b x) (2 c) (3 d)))
: (Listof (Listof Symbol))
'((b x) (c) (d)))
(check-equal?: (map: (compose car cdr) '((1 b x) (2 c) (3 d)))
: (Listof Symbol)
'(b c d))
(check-equal?: (map: (compose add1 car) '((1 b x) (2 c) (3 d)))
: (Listof Number)
'(2 3 4))
#|
(check-equal?: (map: + '(1 2 3) '(4 5 6))
: (Listof Number)
'(5 7 9))|#)

458
graph-lib/graph/map_old.rkt Normal file
View File

@ -0,0 +1,458 @@
#lang debug typed/racket
(require (for-syntax racket/syntax
syntax/stx
syntax/parse
syntax/parse/experimental/template
"../lib/low-untyped.rkt")
(for-meta 2
racket/base
racket/syntax)
"../lib/low.rkt"
"map1.rkt"
"get.lp2.rkt"
"../type-expander/type-expander.lp2.rkt")
(provide map: apply-compose) ;; TODO: move apply-compose to lib/low.rkt
(begin-for-syntax
(define-syntax-class lam
(pattern (~or (~literal λ) (~literal lambda))))
(define-syntax-class mapp
(pattern (~or (~literal map) (~literal map:)))))
(define-for-syntax (remove-identities stx)
(syntax-parse stx
[() #'()]
[((~or (~lit identity) (~lit values) (~lit compose)) . rest)
(remove-identities #'rest)]
[([(~literal compose) . fs] . rest)
(define/with-syntax cleaned-fs (remove-identities #'fs))
(syntax-parse #'cleaned-fs
[() (remove-identities #'rest)]
[(one-f) #`(one-f . #,(remove-identities #'rest))]
[some-fs #`((compose . some-fs) . #,(remove-identities #'rest))])]
[(f . rest)
#`(f . #,(remove-identities #'rest))]))
;; TODO: check that we don't bork the literals identity, values and compose
;; inside macros or function calls, or alter them in any other way, e.g.
;; (map: (compose identity (λ (values) (+ values 1)) identity) '(1 2 3))
;; or
;; (define (calltwice f) (λ (x) (f (f x))))
;; (map: (compose (calltwice identity)) '(1 2 3))
;; Although a poor variable name choice, the two occurences of "values" in the
;; first example shouldn't be altered, and the λ itself shouldn't be touched.
;; In the second one, everything inside the calltwice function call should be
;; left intact.
(define-for-syntax (remove-identities1 stx)
(syntax-parse (remove-identities #`(#,stx))
[() #'identity]
[(f) #'f]))
(begin-for-syntax
(define-syntax-class map-info
(pattern (_ #:in in-type
#:in-∀ [in-∀ ]
#:out out-type
#:∀ (∀-type )
#:arg-funs ([arg-fun
param-fun
(~optional (~and auto-in #:auto-in))
fun-in fun-out] )
#:funs [fun ]))))
(begin-for-syntax
(define-syntax (mk-info stx)
(syntax-case stx ()
[(_ . rest-stx)
(begin
(define/with-syntax whole-stx (syntax/loc stx (info . rest-stx)))
#'(syntax-parse #`whole-stx
[i:map-info #'i]))])))
(define-for-syntax (:map* stx* stx-&ls stx-out stx-out-∀)
(if (stx-null? stx*)
'()
(syntax-parse (:map (stx-car stx*) stx-&ls stx-out stx-out-∀)
[info:map-info
(let ([r (:map* (stx-cdr stx*) stx-&ls #'info.in-type #'[info.in-∀ ])]
[auto (attribute info.auto-in)])
(if (and (not (null? auto)) (car auto) (not (null? r)))
(syntax-parse (car r)
[r-info:map-info
(let ([intact #'([info.arg-fun
info.param-fun
info.fun-in ;;;
info.fun-out] )]
[replaced #'([info.arg-fun
info.param-fun
r-info.out-type ;;info.fun-in ;;;
info.fun-out] )])
(cons (mk-info #:in info.in-type
#:in-∀ [info.in-∀ ]
#:out info.out-type
#:∀ (info.∀-type )
#:arg-funs (#,(stx-car replaced)
#,@(stx-cdr intact))
#:funs [info.fun ])
r))])
(cons #'info r)))])))
(define-for-syntax (:map stx stx-&ls stx-out stx-out-∀)
(define/with-syntax (&l ) stx-&ls)
(define/with-syntax out stx-out)
(define/with-syntax (out-∀ ) stx-out-∀)
(syntax-parse (remove-identities1 stx)
[(~literal car)
#'(info #:in (Pairof out Any) #:in-∀ [] #:out out #:∀ []
#:arg-funs [] #:funs [car])]
[(~literal cdr)
#'(info #:in (Pairof Any out) #:in-∀ [] #:out out #:∀ []
#:arg-funs [] #:funs [cdr])]
;; TODO: should remove `identity` completely, doing (map identity l) is
;; useless appart for constraining the type, but it's an ugly way to do so.
[(~literal identity)
#'(info #:in out #:in-∀ [] #:out out #:∀ []
#:arg-funs [] #:funs [identity])]
[((~literal compose) f )
(syntax-parse (:map* #'(f ) #'(&l ) #'out #'[out-∀ ])
[(~and (_ rightmost:map-info) (leftmost:map-info . _) (:map-info ))
#'(info #:in rightmost.in-type
#:in-∀ [rightmost.in-∀ ];; ??
#:out leftmost.out-type
#:∀ [∀-type ]
#:arg-funs [(arg-fun param-fun fun-in fun-out) ]
#:funs [fun ])])]
[((~literal curry) :mapp f)
(syntax-parse (internal-map: #'f #'(&l ) #'out #'[out-∀ ])
[(i:map-info . code)
#`(info #:in (Listof i.in-type)
#:in-∀ [i.in-∀ ];; ??
#:out (Listof out)
#:∀ [i.∀-type ]; i.out-type
#:arg-funs [(i.arg-fun i.param-fun i.fun-in i.fun-out) ]
#:funs [#,(syntax/loc #'f (code i.fun _))])])]
[(~literal length)
(define-temp-ids "&~a" f)
(define-temp-ids "~a/in" f)
#'(info #:in f/in #:in-∀ [f/in] #:out out #:∀ [f/in] ;;??
#:arg-funs [((λ ([l : (Listof Any)]) (length l))
&f
#:auto-in f/in
out)]
#:funs [&f])]
[((~literal λget) pat )
#'(info #:in (has-get out pat )
#:in-∀ [out-∀ ]
#:out (result-get out pat )
#:∀ []
#:arg-funs []
#:funs [(get _ pat )])]
[f
(define-temp-ids "&~a" f)
(define-temp-ids "~a/in" f)
#'(info #:in f/in #:in-∀ [f/in] #:out out #:∀ [f/in] ;;??
#:arg-funs [(f &f #:auto-in f/in out)] #:funs [&f])]))
(define-syntax (apply-compose stx)
(syntax-parse stx
[(_ [] [a ])
#'(values a )]
[(_ [f (~and loc (f-last x (~literal _) y ))] [a ])
#`(apply-compose [f ] [#,(syntax/loc #'loc (f-last x a y ))])]
[(_ [f f-last] [a ])
#`(apply-compose [f ] [#,(syntax/loc #'f-last (f-last a ))])]))
(define-for-syntax (internal-map: stx-f stx-&ls stx-out stx-out-∀)
(define/with-syntax f stx-f)
(define/with-syntax (&l ) stx-&ls)
(define/with-syntax out stx-out)
(define/with-syntax (out-∀ ) stx-out-∀)
(syntax-parse (:map #'f #'(&l ) #'out #'[out-∀ ])
[(~and i :map-info)
(define/with-syntax map1 (generate-temporary #'map))
(cons #'i
(template
(let ()
(: map1 ( [out-∀ ∀-type ]
( ( fun-in fun-out)
(Listof in-type)
(Listof out-type))))
(define (map1 param-fun &l )
(if (or (null? &l) )
'()
(cons (apply-compose [fun ] [(car &l) ])
(map1 param-fun (cdr &l) ))))
map1)))]));(map1 arg-fun … . ls)
;; TODO: inefficient at compile-time: we run (:map #'f #'Out) twice.
;; Plus it could cause some bugs because of differing #'Out.
(define-syntax (map: stx)
(syntax-parse stx
[(_ (~optional (~and norun (~literal norun))) f l )
(define-temp-ids "&~a" (l ))
(syntax-parse (internal-map: #'f #'(&l ) #'Out #'[Out])
[(:map-info . code)
(if (attribute norun)
#'(ann '(code arg-fun l ) Any)
#'(code arg-fun l ))])]))
(module* test typed/racket
(require (submod "..")
"../lib/low.rkt")
(check-equal?: (map: add1 '(1 2 3))
: (Listof Number)
'(2 3 4))
(check-equal?: (map: (compose add1) '(1 2 3))
: (Listof Number)
'(2 3 4))
(check-equal?: (map: ( identity add1) '(1 2 3))
: (Listof Number)
'(2 3 4))
(check-equal?: (map: ( add1 identity) '(1 2 3))
: (Listof Number)
'(2 3 4))
(check-equal?: (map: ( number->string add1) '(1 2 9))
: (Listof String)
'("2" "3" "10"))
(check-equal?: (map: ( string-length number->string add1) '(1 2 9))
: (Listof Number)
'(1 1 2))
(check-equal?: (map: car '((1 2) (2) (9 10 11)))
: (Listof Number)
'(1 2 9))
(check-equal?: (map: ( add1 car) '((1 2) (2) (9 10 11)))
: (Listof Number)
'(2 3 10))
(check-equal?: (map: ( string-length number->string add1 car cdr)
'((1 2) (2 3) (8 9 10)))
: (Listof Number)
'(1 1 2))
(check-equal?: (map: identity '(1 2 3))
: (Listof Number)
'(1 2 3))
(check-equal?: (map: values '(1 2 3))
: (Listof Number)
'(1 2 3))
(check-equal?: (map: (compose) '(1 2 3))
: (Listof Number)
'(1 2 3))
(check-equal?: (map: (compose identity) '(1 2 3))
: (Listof Number)
'(1 2 3))
(check-equal?: (map: ( identity values identity values) '(1 2 3))
: (Listof Number)
'(1 2 3))
(check-equal?: (map: ( length (curry map add1)) '((1 2) (3)))
: (Listof Number)
'(2 1))
(check-equal?: (map: (curry map add1) '((1 2) (3)))
: (Listof (Listof Number))
'((2 3) (4)))
(define (numlist [x : Number]) (list x))
(check-equal?: (map: ( (curry map add1) numlist) '(1 2 3))
: (Listof (Listof Number))
'((2) (3) (4)))
(check-equal?: (map: ( (curry map add1) (λ ([x : Number]) (list x)))
'(1 2 3))
: (Listof (Listof Number))
'((2) (3) (4)))
;; The tests below using (curry map: …) don't work, because typed/racket wraps
;; the map: identifier with a contract, so the identifier seen outside the
;; module is not the same as the one used in the syntax-parse ~literal clause.
#|(begin
(check-equal?: (map: (curry map add1) '((1 2 3) (4 5)))
: (Listof (Listof Number))
'((2 3 4) (5 6)))
#;(check-equal?: (map: (curry map: add1) '((1 2 3) (4 5)))
: (Listof (Listof Number))
'((2 3 4) (5 6)))
(check-equal?: (map: (curry map (compose number->string add1))
'((1 2 3) (4 5)))
: (Listof (Listof String))
'(("2" "3" "4") ("5" "6")))
#;(check-equal?: (map: (curry map: (compose number->string add1))
'((1 2 3) (4 5)))
: (Listof (Listof String))
'(("2" "3" "4") ("5" "6")))
(check-equal?: (map: add1 '(1 2 3))
: (Listof Number)
'(2 3 4))
(check-equal?: (map: car '((1 a) (2 b) (3 c)))
: (Listof Number)
'(1 2 3))
(check-equal?: (map: (curry map car) '([{1 a} {2 b}] [{3 c}]))
: (Listof (Listof Number))
'([1 2] [3]))
#;(check-equal?: (map: (curry map: car) '([{1 a} {2 b}] [{3 c}]))
: (Listof (Listof Number))
'([1 2] [3]))
(check-equal?: (map: (curry map (curry map car))
'([((1 a) (2 b)) ((3 c))] [((4))]))
: (Listof (Listof (Listof Number)))
'([(1 2) (3)] [(4)]))
#;(check-equal?: (map: (curry map (curry map: car))
'([((1 a) (2 b)) ((3 c))] [((4))]))
: (Listof (Listof (Listof Number)))
'([(1 2) (3)] [(4)]))
#;(check-equal?: (map: (curry map: (curry map car))
'([((1 a) (2 b)) ((3 c))] [((4))]))
: (Listof (Listof (Listof Number)))
'([(1 2) (3)] [(4)]))
#;(check-equal?: (map: (curry map: (curry map: car))
'([((1 a) (2 b)) ((3 c))] [((4))]))
: (Listof (Listof (Listof Number)))
'([(1 2) (3)] [(4)])))|#
(check-equal?: (map: car '((1 b x) (2 c) (3 d)))
: (Listof Number)
'(1 2 3))
(check-equal?: (map: cdr '((1 b x) (2 c) (3 d)))
: (Listof (Listof Symbol))
'((b x) (c) (d)))
(check-equal?: (map: car (map: cdr '((1 b x) (2 c) (3 d))))
: (Listof Symbol)
'(b c d))
(check-equal?: (map: (compose) '((1 b x) (2 c) (3 d)))
: (Listof (Listof (U Number Symbol)))
'((1 b x) (2 c) (3 d)))
(check-equal?: (map: (compose car) '((1 b x) (2 c) (3 d)))
: (Listof Number)
'(1 2 3))
(check-equal?: (map: (compose cdr) '((1 b x) (2 c) (3 d)))
: (Listof (Listof Symbol))
'((b x) (c) (d)))
(check-equal?: (map: (compose car cdr) '((1 b x) (2 c) (3 d)))
: (Listof Symbol)
'(b c d))
(check-equal?: (map: (compose add1 car) '((1 b x) (2 c) (3 d)))
: (Listof Number)
'(2 3 4))
#|
(check-equal?: (map: + '(1 2 3) '(4 5 6))
: (Listof Number)
'(5 7 9))|#)
#|
(map: (compose F (curry map add1)) '((1 2) (3)))
Problem: in the code above, the input type of `F` has to be the return type of
`(curry map add1)`, i.e. `(Listof B)`. The return type of `F` may depend on its
input type (e.g. wrapping a value), so the type information flows leftwards
inside `compose`.
However, if F is a destructuring operation, like `car` or `cdr`, it may impose
constraints on the return type of the function immediately to its right, meaning
that the type information flows rightwards.
It seems difficult to reconcile these two cases without writing a complex
algorithm.
Worst-case scenario:
+-- constrains to the right
v v-- constrains to the right
(compose car complex-calculation (curry map car))
^ ^-- gives a (Listof ?) to the left
+-- constrained on both sides
Maybe we could cover most common cases by first getting the type for the handled
cases which impose constraints to the right and/or give a type to the left, and
then use these types instead of the , to fill in the holes for other functions.
EDIT: that's what we did, using the #:auto-in
|#
#|
(define-for-syntax (map-infer-types stx)
(syntax-parse stx
[(_ (~literal car))
(values #'(A B)
#'(Pairof A B))]
[(_ (~literal cdr)) #'(Pairof Any T)]
[(_ T (~literal values)) #'T]
[(_ T ((~literal compose))) #'T]
[(_ T ((~literal compose) f0 . fs))
#'(map-element (map-element T f0) (compose . fs))]
[(_ T ((~literal curry) (~or (~literal map:) (~literal map)) f) l)
#''_]
;; get
[(_ f . ls)
;; TODO:
#'T]))
(define-type-expander (map-element stx)
(syntax-parse stx
[(_ T:id (~literal car)) #'(Pairof T Any)]
[(_ T:id (~literal cdr)) #'(Pairof Any T)]
[(_ T (~literal values)) #'T]
[(_ T ((~literal compose))) #'T]
[(_ T ((~literal compose) f0 . fs))
#'(map-element (map-element T f0) (compose . fs))]
[(_ T ((~literal curry) (~or (~literal map:) (~literal map)) f) l)
#''_]
;; get
[(_ f . ls)
;; TODO:
#'T]))
(define-type-expander (map-result stx)
(syntax-parse stx
[(_ T:id (~literal car)) #'T]
[(_ T:id (~literal cdr)) #'T]))
(define-syntax (map: stx)
(syntax-parse stx
[(_ (~literal car) l) #'((curry-map A A (Pairof A Any) car) l)]
[(_ (~literal cdr) l) #'((curry-map B B (Pairof Any B) cdr) l)]
;; TODO: add caar etc.
[(_ ((~literal values)) l) #'l]
[(_ ((~literal compose)) l) #'l]
[(_ ((~literal compose) f0 . fs) l) #'(map: f0 (map: (compose . fs) l))]
[(_ ((~literal curry) (~or (~literal map:) (~literal map)) f) l)
#''_]
[(_ ((~literal λget) field-or-accessor ) l)
#'(get l ( ) field-or-accessor )]
[(_ f . ls)
#'(map f . ls)]))
|#
;|#

View File

@ -1,9 +1,9 @@
#lang typed/racket
(module test typed/racket
(require (submod "graph3.lp2.rkt" test))
(require "graph4.lp2.rkt")
(require "map4.rkt")
(require (submod "graph.lp2.rkt" test))
(require "get.lp2.rkt")
(require "map.rkt")
(require "structure.lp2.rkt")
(require "variant.lp2.rkt")
(require "../lib/low.rkt")

23
graph-lib/lib.rkt Normal file
View File

@ -0,0 +1,23 @@
#lang typed/racket
(require racket/require)
(define-syntax-rule (r/p . mods)
(begin
(require . mods)
(provide (all-from-out . mods))))
(r/p "lib/low.rkt"
"type-expander/multi-id.lp2.rkt"
"type-expander/type-expander.lp2.rkt"
"graph/structure.lp2.rkt"
"graph/variant.lp2.rkt"
"graph/graph.lp2.rkt"
"graph/get.lp2.rkt"
"graph/map.rkt"
#|"graph/rewrite-type.lp2.rkt"|#)
(require (subtract-in "graph/dotlang.rkt"
"type-expander/type-expander.lp2.rkt"))
(provide (all-from-out "graph/dotlang.rkt"))

View File

@ -1,11 +1,13 @@
#lang typed/racket
#lang s-exp "lib.rkt"
(require (submod "graph/test-map4-get.rkt" test))
#|
(require (submod "graph/test-map-get.rkt" test))
(require (submod "graph/dotlang.rkt" test))
(require "type-expander/type-expander.lp2.rkt")
(require "type-expander/multi-id.lp2.rkt")
(require "graph/variant.lp2.rkt")
|#
(define-type from (List (Pairof Number Boolean)
(Listof (U Number (Pairof Number String)))))