Upgrade to Pro — share decks privately, control downloads, hide ads and more …

[OpenFest 2020] The Freedom of Static Typing

[OpenFest 2020] The Freedom of Static Typing

Static typing in Rust, a bit of dynamic typing in Ruby, and what "freedom" means for a programming language.

Fc59401781a26b10f5d4fc5b758fb3b7?s=128

Andrew Radev

November 07, 2020
Tweet

Transcript

  1. The Freedom of Static Typing

  2. @AndrewRadev

  3. Rad Dev

  4. Static typing (in Rust)

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

    loops as infinite lists? ✔ M Mo on na ad ds s? ?
  6. Old haskell(ish) talk… about Ruby ✘ Runtime errors! ✘ Chaos!

    Mayhem! ✘ Your language has too much freedom! ✘ You can’t be trusted with it!
  7. Ruby: It’s Really Not That Bad ✔Processes, tools, tests ✔Peer-review

    ✔Error tracking in prod ✔No active desire to see runtime errors :)
  8. 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 ¯\_( ツ )_/¯
  9. Some things really are dangerous https://twitter.com/thegrugq/status/1141945136122740736 free() free()

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

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

  12. Positive examples

  13. Enums (Algebraic data types) (Sum types)

  14. enum Weekday { Monday, Tuesday, Wednesday, Thursday, Friday, } enum

    IpAddr { V4(u8, u8, u8, u8), V6(String), } let home = IpAddr::V4(127, 0, 0, 1);
  15. Option and Result

  16. enum Option<T> { Some(T), None, } fn main() { let

    args = vec![1, 2, 3]; let third = args.get(2); // => Some(3) let missing = args.get(100); // => None }
  17. enum Option<T> { Some(T), None, } fn main() { let

    args = vec![1, 2, 3]; // Pattern matching with `match`: match args.get(2) { Some(value) => println!("Third arg is: {}", value), None => println!("No third arg :/"), } }
  18. enum Option<T> { Some(T), None, } fn main() { let

    args = vec![1, 2, 3]; // Pattern matching with `if let`: if let Some(value) = args.get(2) { println!("Third arg is: {}", value); } }
  19. enum Result<T, E> { Ok(T), Err(E), } fn main() {

    // settings: Result<String, io::Error> let settings = fs::read_to_string("~/settings.json"); // But why not a pair (String, io::Error)? let (settings, error) = fs::read_to_string("~/settings.json"); }
  20. fn read_settings(path: &str) -> Result<String, Box<dyn Error>> { // ...

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

    @value = value @error = error end end
  22. 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
  23. # Successful result: Result.new(value: "Pumpkin spice latte") # Errorful result:

    Result.new(error: "Ran out of pumpkins!") result. or_else { |e| ... }. and_then { |v| ... } # Is this possible? :/ What do we do here? Result.new( value: "Everything is great!", error: "Everything is broken!" )
  24. class Result def and_then(&block) if value # ... end end

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

    # ... @alchemiter = ... @laserstation = ... @punch_designix = ... # ... end end
  26. enum Event { Initialization { alchemiter: ..., laserstation: ..., },

    Cascade { laserstation: ..., punch_designix: ..., }, // ... }
  27. Types as containers • Classes/Structs • Pairs/Tuples • Lists •

    Dictionaries
  28. Enums: not just containers Enums express exclusivity

  29. Enums in dynamic languages?

  30. class Success def and_then(&block) block.call(value) end def or_else(&block) # do

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

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

    an Err
  33. Implicit knowledge vs Explicit rules

  34. (Implicit != “bad”)

  35. (Implicit != “bad”) • Some things can be considered “common

    sense” • Potentially richer rules • Easier/faster to patch rules
  36. result = Result.new(value: [], error: []) ComplicatedProcess.each do |individual_result| if

    individual_result.success? result.value << individual_result else result.error << "Something went wrong with #{individual_result}" end end # (Basic usage of `Result`, no `and_then`/`or_else`) # Temporary/intermediate step – a patch
  37. Monkey-patching

  38. Monkey-patching F Fr re ee ed do om m-patching!

  39. # In code class UserRegistration def facebook_client FacebookClient.new end end

    # In tests class UserRegistration # Patch it! def facebook_client FakeFacebookClient.new end end
  40. class TrueClass def if_true(&block) block.call self end def if_false(&block) self

    end end
  41. class FalseClass def if_true(&block) self end def if_false(&block) block.call self

    end end
  42. (2 + 2 == 5). if_true { print "Radiohead were

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

    if_false<F: FnOnce()>(self, F) -> Self; }
  44. 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 } }
  45. fn main() { (6 * 9 == 42). if_true(|| println!("Don't

    forget your towel!")). if_false(|| println!("More boring math.")); }
  46. Monkey-patching

  47. Core Extensions ✘ Can’t override existing methods ✘ Can’t add

    new fields ✔Modular, encapsulated
  48. require 'active_support/core_ext/string/inflections' "table".pluralize # => "tables" "ox".pluralize # => "oxen"

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

    println!("{}", "ox".to_plural()); // => "oxen" } // "Use" statement only affects a limited scope
  50. use inflector::Inflector; fn main() { println!("{}", Inflector::to_plural("table")); // => "tables"

    println!("{}", Inflector::to_plural("ox")); // => "oxen" } // "Use" statement only affects a limited scope
  51. Itertools

  52. 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>
  53. impl<T: ?Sized> Itertools for T where T: Iterator { }

  54. impl<T: ?Sized> Itertools for T where T: Iterator { }

  55. impl<T: ?Sized> Itertools for T where T: Iterator { }

  56. impl<T: ?Sized> Itertools for T where T: Iterator { }

  57. 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 }
  58. 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 }
  59. 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 }
  60. None
  61. None
  62. Macros?

  63. Macros: Not exactly a static typing thing

  64. fn build_html() -> Result<Html, io::Error> { let template = match

    fs::read_to_string("template.html") { Ok(string) => string, Err(e) => return Err(e), }; let data = match fs::read_to_string("data.json") { Ok(string) => string, Err(e) => return Err(e), }; // ... }
  65. macro_rules! try! { (expr:$expr) => { match $expr { Ok(string)

    => string, Err(e) => return Err(e), } } } fn build_html() -> Result<Html, io::Error> { let template = try!(fs::read_to_string("template.html")); let data = try!(fs::read_to_string("data.json")); // ... }
  66. fn get_settings() -> Result<Settings, ???> { let settings = match

    fs::read_to_string("/etc/settings") { Ok(string) => string, Err(e) => return Err(e), }; let parsed_settings = match parse_settings(settings) { Ok(settings) => settings, Err(e) => return Err(e), }; // ... }
  67. fn get_settings() -> Result<Settings, ???> { let settings = match

    fs::read_to_string("/etc/settings") { Ok(string) => string, Err(e) => return Err(e), // => io::Error }; let parsed_settings = match parse_settings(settings) { Ok(settings) => settings, Err(e) => return Err(e), // => JsonParseError }; // ... }
  68. impl From<io::Error> for AppError { fn from(source: io::Error) -> Self

    { // ... } } impl From<JsonParseError> for AppError { fn from(source: JsonParseError) -> Self { // ... } }
  69. impl From<io::Error> for AppError { /*...*/ } impl From<JsonParseError> for

    AppError { /*...*/ } fn get_settings() -> Result<Settings, AppError> { let settings = match fs::read_to_string("/etc/settings") { Ok(string) => string, Err(e) => return Err(AppError::from(e)), // => AppError }; let parsed_settings = match parse_settings(global_settings) { Ok(settings) => settings, Err(e) => return Err(AppError::from(e)), // => AppError }; // ... }
  70. impl From<io::Error> for AppError { /*...*/ } impl From<JsonParseError> for

    AppError { /*...*/ } macro_rules! try! { (expr:$expr) => { match $expr { Ok(string) => string, Err(e) => return Err(AppError::from(e)), } } } fn get_settings() -> Result<Settings, AppError> { let global_settings = try!(fs::read_to_string("/etc/settings")); let parsed_settings = try!(parsed_settings(global_settings)); // ... }
  71. Return-type Polymorphism (Generic Returns)

  72. 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) } }
  73. // 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!"); } }
  74. search search_and_get?

  75. 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; }
  76. 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> { // ... } }
  77. 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); }
  78. 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); } }
  79. 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); } }
  80. Serialization/Deserialization

  81. 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, }
  82. fn example() -> Result<()> { let raw_data = r#" {

    "title": "OpenFest 2020", "body": "This year, it’s gotta be online :/", "comments": [ { "author_name": "Z", "body": "That’s rough, buddy" } ] } "#; let article: Article = serde_json::from_str(raw_data)?; let comments: CommentList = serde_json::from_str(raw_data)?; // ... Ok(()) }
  83. Return-type polymorphism ✔It’s magic!

  84. let article: Article = serde_json::from_str(raw_data)?; let comments: CommentList = serde_json::from_str(raw_data)?;

  85. Return values Dynamic typing: Functions can return different types Static

    typing: Callers can request different types
  86. Return-type polymorphism ✔It’s magic! ✔Extra degree of freedom in interface

    design
  87. macro_rules! try! { (expr:$expr) => { match $expr { Ok(string)

    => string, Err(e) => return Err(AppError::from(e)), } } } fn get_settings() -> Result<Settings, AppError> { let global_settings = try!(fs::read_to_string("/etc/settings")); let parsed_settings = try!(parsed_settings(global_settings)); // ... }
  88. impl From<io::Error> for AppError { /*...*/ } impl From<JsonParseError> for

    AppError { /*...*/ } impl From<Source> for Target { // ... } impl<...> Into<Target> for Source where Target: From<Source> { fn into(self) -> Target { Target::from(self) } } // AppError::from(e) == e.into()
  89. macro_rules! try! { (expr:$expr) => { match $expr { Ok(string)

    => string, Err(e) => return Err(AppError::from(e)), } } }
  90. macro_rules! try! { (expr:$expr) => { match $expr { Ok(string)

    => string, Err(e) => return Err(e.into()), } } }
  91. macro_rules! try! { (expr:$expr) => { match $expr { Ok(string)

    => string, Err(e) => return Err(e.into()), } } } fn get_settings() -> Result<Settings, AppError> { let global_settings = try!(fs::read_to_string("/etc/settings")); let parsed_settings = try!(parsed_settings(global_settings)); // ... }
  92. macro_rules! try! { (expr:$expr) => { match $expr { Ok(string)

    => string, Err(e) => return Err(e.into()), } } } fn get_settings() -> Result<Settings, AppError> { let global_settings = fs::read_to_string("/etc/settings")?; let parsed_settings = parsed_settings(global_settings)?; // ... }
  93. Return-type polymorphism ✔It’s magic! ✔Extra degree of freedom in interface

    design ✔Control over the “shape” of code ✘ It’s magic :/ ✘ Can hurt understanding
  94. GTK-rs let label1 = gtk::Label::new(Some("Hello")); let label2 = gtk::Label::new(None); impl

    Label { fn new(s: Option<&str>) -> Label { // ... } }
  95. 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(); // ... } }
  96. 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
  97. 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 ;) ;) ;)
  98. Freedom... ✔...means allowing you to express concepts in code ✔...means

    allowing you to write bad code :)
  99. Freedom... ✔...to return whatever type you want ✔...to request whatever

    type you want
  100. Freedom... ✔...to patch global types ✔...to distribute extensions as libraries

  101. Freedom... ✔...is easier to see in a dynamically-typed language

  102. The Freedom of Static Typing

  103. The Freedom of Static Typing @AndrewRadev https://andrewra.dev