Matthias and Sam, hello. This has been a very useful brief thread for me, and also, I suspect, for others.
On 2014 Aug 4, at 01:22, Matthias Felleisen <matth...@ccs.neu.edu> wrote: > In a sense, you put your finger on a sore spot of TR from R -- we are still > figuring out how to help programmers along here. Such help would be most valuable! > ;; entirely unnecessary but helped me read; IGNORE > (define-type (Maybe α) (U False α)) > (define-type NI Nonnegative-Integer) > (define-type LNI (Listof NI)) > (define-type VNI (Vectorof NI)) This is clearly a good idiom/pattern: very local, and telegraphically short, abbreviations for useful types. > Then I checked on the maps and realized that I need to help the TC along with > the instantiations of vector->list (how can it know with context-free > reasoning what kind of vector it is) and the same for car (how can it know > what kind of list you're getting here). So this returns to the question of what the TC can and cannot be expected to work out for itself, and I see that Sam had a couple of remarks on this later. > (let* ((given : LVNI '(#(1 2) #(3 4) #(5 6))) > (length : NI (1+ (apply max (map (lambda ({x : VNI}) (vector-ref x > 0)) given)))) > (result : [Vectorof [Maybe NI]] (make-vector length #f))) > (for ((q : VNI (in-list given))) > (vector-set! result (vector-ref q 0) (vector-ref q 1))) > result) > > The above -- minus types -- is how I would probably write the code in Racket, > following the style guide. (I would use three defines instead of let* but > let's ignore this point.) That looks _much_ nicer than what I had, and is still perfectly readable. > If I were to move this code to Typed Racket, I'd add a type declaration to > each identifier just like in any other (explicitly, statically) typed > programming language. That's what I did, and there was no problem checking > and running your code. So the pattern I see there, which both you and Sam have illustrated, is: * annotate as far 'down' the tree as possible, so that type information can propagate upwards in the parse tree * If one liberally uses named bindings (recommended on other style grounds, in any case), then each of these is a natural location for a type annotation * when a polymorphic-typed function takes polymorphic arguments, annotate the arguments, not the function (that is, I see that, in (map car '(foo ...)) both you and Sam annotated the car, not the map); this is in harmony with 'annotate as far down as possible', for some value of 'down'. Sam said: > The most signficant differences are (1) I'm not using `cast`, which > should be avoided when possible. `cast` is a runtime operation, and > might be hiding arbitrary bugs. (2) I've annotated the `let`-bound > variables, which makes life easier on the rest of the type checker and > helps improve error messages. Ah, now this is something I've meant to ask about: what's the difference between ann and cast? The manual says: > (ann e t) > Ensure that e has type t, or some subtype. The entire expression has type t. > This is legal only in expression contexts. > (cast e t) > The entire expression has the type t, while e may have any type. The value of > the entire expression is the value returned by e, protected by a contract > ensuring that it has type t. This is legal only in expression contexts. I've never been sure whether the difference between "Ensure that e has type t" (which sounds rather runtime-y) and "The entire expression has the type t" is significant. The only difference I could parse from this is that `cast` includes a contract, which would appear to make it _safer_ than `ann`, in the sense that if this were accidentally given a wrong-typed value than it would cause an immediate error. I presume that the difference is that `ann` is about static type analysis, whereas `cast` is for the situation where that static analysis can't be done. Would I be correct that in the above case I can and should write (ann '(#(1 2) #(3 4) #(5 6)) (Listof VNI) since this is saying that this expression has a more specific type than the TC would work out by itself, and it can verify that the restriction is possible. In the real program, these numbers appear from a SQL query, and so this list is actually of type (Listof (Vectorof (U Integer String))), so that I should indeed be writing (cast (query-rows ...) (Listof VNI)) because these numbers are coming in, dynamically, from _outside_ the reach of the static TC, and so the annotation needs to be dynamically verified at this point. That is: `cast` is for the 'boundary' of the type system, whereas `ann` is for adding detail within it? Ah -- I think I've just had a little epiphany. In which case, it might be useful to transplant a little of that into the ann/cast section of the manual. The examples shown there don't really bring out the distinction to the naive reader. (Just by the way, `assert` seems to be doing rather similar work to these two, and while I can appreciate a distinction, from the text, I'd struggle to express it compactly -- a compare-and-contrast explanation in the manual would be very illuminating) (Does this imply that when TR code is called from untyped code, the arguments are implicitly `cast` at call-time?) > There are a few things that make this program require more annotations > than I'd like. > > 1. TR doesn't handle automatically inferring type arguments when > polymorphic functions (like `map`) are given polymorphic arguments > like `vector->list`. While fixing this would require work, it just > needs to be done -- there's no reason we shouldn't be able to make > this work. Righto -- that's what I wondered, whether it was 'just a matter of code' or whether there was something fundamentally more complicated about this, which I wasn't understanding. Thanks, both, for your careful explanations. Best wishes, Norman -- Norman Gray : http://nxg.me.uk SUPA School of Physics and Astronomy, University of Glasgow, UK ____________________ Racket Users list: http://lists.racket-lang.org/users