The Freedom of Static Typing

The Freedom of Static Typing

It’s a common idea: Dynamic typing gives you freedom to do whatever you want, but then you suffer mistakes and runtime errors. Static typing limits your freedom, but it leads to fewer bugs, even if it’s harder to actually write code.

This isn’t entirely false. Yes, it can be quite hard to write a Rust or Haskell program that compiles, enough that it’s a running gag in their communities. “But once you get it to compile, it’s probably correct!”, the joke goes. On the other hand, when writing Ruby or Javascript, you’re free to call functions with all sorts of things, and then runtime errors can easily slip into production. That said, it’s not as simple as a sliding scale between “freedom” and “safety”. Neither of these terms is one-dimensional – a language feature that restricts your freedom in one way can actually free you in different ways.

I’d like to demonstrate some interesting language features in Rust that make it easier to write code and to express concepts compared to dynamic languages. Features that don’t just protect you from shooting your foot, but give you power that’s only available within the rules and “limitations” of a statically typed language. Even if you’re not familiar with Rust, I hope to focus the examples on the features themselves rather than on the specifics of the language, so come by and let’s talk about being (type-safely) free.

Fc59401781a26b10f5d4fc5b758fb3b7?s=128

Andrew Radev

October 03, 2019
Tweet

Transcript

  1. The Freedom of Static Typing

  2. @AndrewRadev

  3. Rad Dev

  4. @AndrewRadev Static typing (in Rust)

  5. @AndrewRadev Old haskell(ish) talk Return-type polymorphism? Algebraic data types? Modeling

    event loops as infinite lists? M Mo on na ad ds s? ?
  6. @AndrewRadev Old haskell(ish) talk Runtime errors! “Mathematically impossible to have

    errors in <language>” Your language has too much freedom! You can’t be trusted with it!
  7. @AndrewRadev Ruby: It’s Really Not That Bad # This is

    good: Person.walk_the_dog(Dog.new) # But WHAT IF? Person.walk_the_dog(Cat.new) # Solution: don’t do that ¯\_( ツ )_/¯
  8. @AndrewRadev Ruby: It’s Really Not That Bad Processes, tools, tests

    Peer-review Error tracking in prod
  9. @AndrewRadev Some things really are dangerous https://twitter.com/thegrugq/status/1141945136122740736 free() free()

  10. @AndrewRadev Static typing as a… necessary limitation? :/

  11. @AndrewRadev Static typing as a form of freedom! :)

  12. @AndrewRadev Enums (Algebraic data types) (Sum types)

  13. @AndrewRadev enum Weekday { Monday, Tuesday, Wednesday, Thursday, Friday, }

    enum IpAddr { V4(u8, u8, u8, u8), V6(String), }
  14. @AndrewRadev enum Option<T> { Some(T), None, } fn main() {

    let args = vec![1, 2, 3]; if let Some(value) = args.get(2) { println!("Third arg is: {}", value); } else { println!("Nope, nothing here"); } }
  15. @AndrewRadev enum Result<T, E> { Ok(T), Err(E), } fn read_file(path:

    &str) -> Result<String, io::Error> { // ... } fn main() { // settings: Result<T, E> let settings = read_file("~/settings.json"); // But why not a pair (T, E)? let (settings, error) = read_file("~/settings.json"); }
  16. @AndrewRadev fn read_file(path: &str) -> Result<String, Box<dyn Error>> { //

    ... } fn parse_settings(json: &str) -> Result<Settings, Box<dyn Error>> { // ... } fn main() { let settings = read_file("~/settings.json"). or_else(|_| read_file("~/.config/settings.json")). and_then(|json| parse_settings(&json)). unwrap_or_else(|_| Settings::default()); println!("{:?}", settings); }
  17. @AndrewRadev class Result attr_reader :value, :error def initialize(value: nil, error:

    nil) @value = value @error = error end end
  18. @AndrewRadev class Result def and_then(&block) if value block.call(value) else self

    end end def or_else(&block) if error block.call(error) else self end end end
  19. @AndrewRadev # Successful result: Result.new(value: "Pumpkin spice latte") # Errorful

    result: Result.new(error: "Ran out of pumpkins!") # Is this possible? :/ What do we do here? Result.new( value: "Everything is great!", error: "Everything is broken!" )
  20. @AndrewRadev class Result def and_then(&block) if value # ... end

    end def or_else(&block) if error # ... end end end # Add runtime checks?
  21. @AndrewRadev class Event def initialize(...) @type = "initialization" @type =

    "cascade" # ... @alchemiter = ... @laserstation = ... @punch_designix = ... # ... end end
  22. @AndrewRadev enum Event { Initialization { alchemiter: ..., laserstation: ...,

    }, Cascade { laserstation: ..., punch_designix: ..., }, // ... }
  23. @AndrewRadev Types as containers Classes/Structs Pairs/Tuples Lists Dictionaries

  24. @AndrewRadev Enums: not just containers Enums express exclusivity

  25. @AndrewRadev class Success def and_then(&block) block.call(value) end def or_else #

    do nothing end end class Error def and_then(&block) # do nothing end def or_else block.call(error) end end
  26. @AndrewRadev Dynamic typing Implicit knowledge: Success and Error should have

    the same methods. Implicit knowledge: Result has either a value or an error
  27. @AndrewRadev Static typing Explicit rule: Result is either an Ok

    or an Err
  28. @AndrewRadev Implicit knowledge vs Explicit rules

  29. @AndrewRadev (Implicit != “bad”) (“common sense”) (potentially richer rules) (easier/faster

    to patch rules)
  30. @AndrewRadev Monkey-patching

  31. @AndrewRadev Monkey-patching ✨F Fr re ee ed do om m-patching!

  32. @AndrewRadev # In code class UserRegistration def facebook_client FacebookClient.new end

    end # In tests class UserRegistration def facebook_client FakeFacebookClient.new end end
  33. @AndrewRadev class TrueClass def if_true(&block) block.call self end def if_false

    self end end
  34. @AndrewRadev class FalseClass def if_true self end def if_false(&block) block.call

    self end end
  35. @AndrewRadev (2 + 2 == 5). if_true { print "Radiohead

    were right!\n" }. if_false { print "Math is still boring, I guess\n" }
  36. @AndrewRadev trait FancyIf { fn if_true<F: FnOnce()>(self, F) -> Self;

    fn if_false<F: FnOnce()>(self, F) -> Self; }
  37. @AndrewRadev impl FancyIf for bool { fn if_true<F: FnOnce()>(self, action:

    F) -> Self { if self { action() } self } fn if_false<F: FnOnce()>(self, action: F) -> Self { if !self { action() } self } }
  38. @AndrewRadev fn main() { (6 * 9 == 42). if_true(||

    println!("Don't forget your towel!")). if_false(|| println!("More boring math.")); }
  39. @AndrewRadev Monkey-patching

  40. @AndrewRadev Core Extensions Can’t override existing methods Can’t add new

    fields Modular, encapsulated
  41. @AndrewRadev require 'active_support/core_ext/string/inflections' "table".pluralize # => "tables" "ox".pluralize # =>

    "oxen" # String#pluralize is now *everywhere* # (Refinements are a possible solution)
  42. @AndrewRadev use inflector::Inflector; fn main() { println!("{}", "table".to_plural()); // =>

    "tables" println!("{}", "ox".to_plural()); // => "oxen" } // "Use" statement only affects a limited scope
  43. @AndrewRadev Itertools

  44. @AndrewRadev The Iterator trait "The quick brown Firefox...".chars() // =>

    Chars "jumps over the lazy Netscape".bytes() // => Bytes vec!["Veni", "Vidi", "Vec!"].into_iter() // => vec::IntoIter<T> let mut passwords = HashMap::new(); passwords.insert("root", "admin123"); passwords.values() // => hash_map::Values<K, V>
  45. @AndrewRadev impl<T: ?Sized> Itertools for T where T: Iterator {

    }
  46. @AndrewRadev impl<T: ?Sized> Itertools for T where T: Iterator {

    }
  47. @AndrewRadev impl<T: ?Sized> Itertools for T where T: Iterator {

    }
  48. @AndrewRadev impl<T: ?Sized> Itertools for T where T: Iterator {

    }
  49. @AndrewRadev Itertools fn main() { let letter_table = "Chunky raisins".chars().

    chunks(7).into_iter(). map(|chunk| chunk.intersperse('|').collect()). intersperse("-+-+-+-+-+-+-".to_string()). join("\n"); println!("{}", letter_table); // C|h|u|n|k|y| // -+-+-+-+-+-+- // r|a|i|s|i|n|s }
  50. None
  51. None
  52. @AndrewRadev Macros?

  53. @AndrewRadev Macros?

  54. @AndrewRadev Return-type Polymorphism (Generic Returns)

  55. @AndrewRadev use regex::Regex; struct Article { title: String, body: String,

    } impl Article { fn new(title: &str, body: &str) -> Self { // ... } fn search(&self, r: &Regex) -> bool { r.is_match(&self.title) || r.is_match(&self.body) } }
  56. @AndrewRadev // use regex::Regex; // struct Article {...} // impl

    Article {...} fn main() { let input = Article::new( "Hello!", "My email is andrew@fakemail.dev!" ); let pattern = Regex::new(r"\S+@\S+\.\S+").unwrap(); if input.search(&pattern) { println!("It's a match!"); } }
  57. search search_and_get?

  58. @AndrewRadev use regex::Regex; struct Article { title: String, body: String,

    } impl Article { fn new(title: &str, body: &str) -> Self { // ... } } trait Search<T> { fn search(&self, r: &Regex) -> T; }
  59. @AndrewRadev impl Search<bool> for Article { fn search(&self, r: &Regex)

    -> bool { // ... } } impl Search<Option<String>> for Article { fn search(&self, r: &Regex) -> Option<String> { // ... } } impl Search<Vec<String>> for Article { fn search(&self, r: &Regex) -> Vec<String> { // ... } }
  60. @AndrewRadev fn main() { let pattern = Regex::new(r"\S+@\S+\.\S+").unwrap(); let input

    = Article::new( "Hello!", r#" My email is andrew@fakemail.dev! I've got another one at andrew@ra.dev! "# ); let is_match: bool = input.search(&pattern); let actual_match: Option<String> = input.search(&pattern); let all_the_matches: Vec<String> = input.search(&pattern); }
  61. @AndrewRadev fn main() { let pattern = Regex::new(r"\S+@\S+\.\S+").unwrap(); let input

    = Article::new( "Hello!", r#" My email is andrew@fakemail.dev! I've got another one at andrew@ra.dev! "# ); if input.search(&pattern) { println!("Yes, a match!"); } if let Some(m) = input.search(&pattern) { println!("Yes, a match: {}!", m); } }
  62. @AndrewRadev fn main() { let pattern = Regex::new(r"\S+@\S+\.\S+").unwrap(); let input

    = Article::new( "Hello!", r#" My email is andrew@fakemail.dev! I've got another one at andrew@ra.dev! "# ); let all_the_matches: Vec<String> = input.search(&pattern); for m in all_the_matches { println!("One of the matches is: {}", m); } }
  63. @AndrewRadev Serialization/Deserialization

  64. @AndrewRadev use serde::Deserialize; #[derive(Debug, Deserialize)] struct Article { title: String,

    body: String, } #[derive(Debug, Deserialize)] struct CommentList { comments: Vec<Comment>, } #[derive(Debug, Deserialize)] struct Comment { author_name: String, body: String, }
  65. @AndrewRadev fn example() -> Result<()> { let raw_data = r#"

    { "title": "Bonjour le monde", "body": "Look at moi, I am speaking Française!", "comments": [ { "author_name": "A Troll", "body": "l8me!!!!!!!!" } ] } "#; let article: Article = serde_json::from_str(raw_data)?; let comments: CommentList = serde_json::from_str(raw_data)?; println!("{:#?}", article); println!("{:#?}", comments); Ok(()) }
  66. @AndrewRadev Return-type polymorphism It’s magic!

  67. @AndrewRadev let article: Article = serde_json::from_str(raw_data)?;

  68. @AndrewRadev Return values Dynamic typing: Functions can return different types

    Static typing: Callers can request different types
  69. @AndrewRadev Return-type polymorphism It’s magic :/ Can hurt understanding It’s

    magic! Extra degree of freedom in interface design
  70. @AndrewRadev GTK-rs let label1 = gtk::Label::new(Some("Hello")); let label2 = gtk::Label::new(None);

    impl Label { fn new(s: Option<&str>) -> Label { // ... } }
  71. @AndrewRadev GTK-rs let label1 = gtk::Label::new("Hello"); let label2 = gtk::Label::new(None);

    impl Label { fn new(s: impl Into<Option<&str>>) -> Label { let s = s.into(); // ... } }
  72. @AndrewRadev GTK-rs There are two reasons behind going back on

    this feature: The first one is about [...] error messages. [...] annoying situations where you had to try to figure out what was going on. The second one is to go back to some Rust fundamentals: being explicit. […] – The GTK-rs team
  73. @AndrewRadev The freedom to make mistakes fn schroedingers_cat<T>() -> Box<T>;

    // This makes sense: fn schroedingers_cat() -> Box<LivingCat> { } fn schroedingers_cat() -> Box<DeadCat> { } // But WHAT IF? fn schroedingers_cat() -> Box<Dog> { } // Don’t do that ;) ;) ;)
  74. @AndrewRadev Freedom... ...means allowing you to express concepts in code

    ...means allowing you to write bad code :) ...is not just about type signatures ...is not one dimensional
  75. The Freedom of Static Typing