#lang racket #| Let's reflect: mutable vs immutable Pros of immutable: - programs are easier to reason about (no history/future involved) (helps humans + compilers) - every bug becomes a local bug, detectable w/ unit tests - natural match for parallel cores Cons of immutable: - takes more memory (up to 2x, if gc good) (But, sharing memory is much easier/safer.) - takes more time to copy objects, rather than mutate one field (And, some gc strategies can be unpredictable; not for real-time.) Think about the cons above vs the pros below, in our asteroids implementation. Were time,memory issues? Was the mental model difficult? Pros of mutable: - more natural mental model (an object changes some of its state over time) ? - easy to have multiple methods share changes to a single, central, up-to-date list. Cons of mutable: - easy to have multiple methods inadvertently share changes. - Can lead to incredibly hard-to-find bugs. Consider our function for AncTrees, allNames (returns a list). Since our trees our immutable (we don't export any setters), we know that our contents never change; we might optimize by *caching* our resulting list-of-names. On future calls, we'll just return our cached list. HOWEVER, in Java, the returned-list is mutable. So whoever got the list the first time might delete things from it. They're deleting from our cached copy! The second person calling our method complains that our code doesn't work (even though we check it and it passes all our tests, and the bug is not particularly anywhere in our code -- it's in the code of another class.) Tracking such errors down can be very difficult. The 'solution' is to not cache, or to clone our cached list when return lists [which works against the whole advantage to caching] ... regardless, it's more we have to reason about, when there is mutation in the language. Btw, Java made String and the wrapper classes (Double, ...) all immutable. My first reaction (as a C programmer) was "wow, I mutate strings all the time; if using Java, I'll be taking a big performance hit whenever I call toUpper, etc." However, time has shown that immutable strings tend to work just fine, and are not any sort of show-stopping performance issue. |# #| I just learned: Hey, there is a method: List Collections.unmodifiableList( Collection ) that returns an immutable list. To be precise: it's still class List, so you can *call* the methods add, delete, etc. (in addition to get, size, etc) However the methods add,delete,etc are guaranteed to throw an error. (Support the method, but promise you'll throw an exception if it's ever called? That's kinda a weird solution! It works, but it's reneging on some of the benefits that a type-system gave us, like the guarantee that the object will implement certain behaviors. Morally, it's a type-error we're getting at run-time: "you have an object that doesn't really support `add`, but you called that method anyway, and the type system couldn't catch it." Perhaps better, there would be an agnostic 'list' with just a few methods, and two subclasses ImmutableList and MutableList.) |# #| Let's reflect: type-checking Type-checking: Java -- static -- typesafe -- *proves* that your code won't misinterpret a bit-pattern as a wrong type, and proceed incorrectly. (If casting is used, then the exception is moved from compile-time to run-time, but it's still considered type-safe: we still won't proceed based on bad bits.) Racket -- run-time -- typesafe, but only at run-time: primitive methods check their input-types. (So the language definitely *has* types, even though you don't need to declare them.) C -- NOT typesafe: bits interpreted as-is; compiler discourages mistakes but the programmer can type-cast, which (unlike Java) doesn't change the compiled code; it only suppresses compiler-errors. php -- plays loosy-goosy by declaring everything convertable to/from strings, and does those conversions as necessary, and declares it correctly typed ?? |# #| If you want type-checking (run-time) in racket: Check out http://www.radford.edu/~itec380/2011fall-ibarland/Lectures/contract-examples.rkt |# #| Grammar for our N0 project: Expr ::= Number | Var | ParenExpr | BinExpr | IfZExpr ParenExpr ::= ( Expr ) BinExpr ::= ( Expr BinOp Expr ) IfZExpr ::= if Expr is0 then Expr else Expr ; BinOp ::= plus | minus | times Number ::= any numeric literal, a la Java or Racket, your choice Var ::= any string with no whitespace, that is not one of the reserved words “plus”, is0, etc.. |# ; We need data def'ns for our union types; ; we'll make real code out of the union-type-def'n #| An Expr is: one of - number - string - (make-paren-expr e) - (make-bin-expr e1 op e2) - (make-ifZ-expr tst thn els) |# ; The natural template for *any* function handling an Expr: ; func-for-expr : expr -> ??? ; (define (func-for-expr ex) (cond [(number? ex) ..ex..] [(string? ex) ..ex..] [(paren-expr? ex) ..(func-for-expr (paren-expr-e ex))..] [(bin-expr? ex) ..(func-for-expr (bin-expr-e1 ex)) ..(bin-expr-e2 ex) ..(func-for-expr (bin-expr-op ex))] [(ifz-expr? ex) ..(func-for-expr (ifz-expr-tst ex)) ..(func-for-expr (ifz-expr-thn ex)) ..(func-for-expr (ifz-expr-els ex))] )) ; expr? : ANY -> boolean ; A type-predicate (can be useful with define/contract, if we use that): (define (expr? any-val) (or (number? any-val) (string? any-val) (paren-expr? any-val) (bin-expr? any-val) #;(if-Z-expr? any-val))) (define (bin-op? any-val) (member any-val (list 'plus 'minus 'times))) ; Examples of using define-struct/contract, ; to add types to our structs: (define-struct/contract bin-expr ([left expr?] [op bin-op?] [right expr?]) #:transparent) (define-struct/contract paren-expr ([e expr?]) #:transparent) "x" (make-bin-expr 3 'plus 4) (make-bin-expr (make-bin-expr 3 'plus 4) 'minus 5)