├── .gitignore ├── Procfile ├── assets ├── favicon.png ├── favicon.svg └── ruudvanasseldonk.asc ├── docindex ├── build.py ├── get_stars.py ├── index.html ├── readme.md ├── repos.toml └── stars.json ├── flake.lock ├── flake.nix ├── fonts ├── extra │ ├── math-italic.sfd │ └── math-upright.sfd ├── generate.py ├── readme.md └── subset.py ├── images ├── build.ninja ├── compress.sh ├── compressed │ ├── build.svg │ ├── geomancer-move-mage.jpg │ ├── geomancer-overview.jpg │ ├── geomancer-soldier-under-attack-by-mage.jpg │ ├── ill-build-my-own-configuration-language.png │ ├── lattice.svg │ ├── rectangles.svg │ ├── richys-groceries.jpg │ ├── robigo-luculenta.jpg │ ├── subtypes.svg │ ├── the-small-bang-theory.jpg │ └── tristar-cyan.png ├── crush.sh ├── original │ ├── build.svg │ ├── geomancer-move-mage.png │ ├── geomancer-overview.png │ ├── geomancer-soldier-under-attack-by-mage.png │ ├── ill-build-my-own-configuration-language.png │ ├── lattice.svg │ ├── rectangles.svg │ ├── richys-groceries.png │ ├── robigo-luculenta.png │ ├── subtypes.svg │ ├── the-small-bang-theory.png │ └── tristar-cyan.png ├── resize.sh └── resized │ ├── geomancer-move-mage.png │ ├── geomancer-overview.png │ ├── geomancer-soldier-under-attack-by-mage.png │ ├── richys-groceries.png │ ├── robigo-luculenta.png │ ├── the-small-bang-theory.png │ └── tristar-cyan.png ├── licence ├── posts ├── a-float-walks-into-a-gradual-type-system.md ├── a-language-for-designing-slides.md ├── a-perspective-shift-on-amms-through-mev.md ├── a-reasonable-configuration-language.md ├── a-type-system-for-rcl-part-1-introduction.md ├── a-type-system-for-rcl-part-2-the-type-system.md ├── a-type-system-for-rcl-part-3-related-work.md ├── ai-alignment-starter-pack.md ├── an-algorithm-for-shuffling-playlists.md.m4 ├── an-api-for-my-christmas-tree.md ├── build-system-insights.md ├── building-elm-with-stack.md ├── continuations-revisited.md ├── csharp-await-is-the-haskell-do-notation.md ├── encryption-by-default.md ├── exceptional-results-error-handling-in-csharp-and-rust.md ├── fibonacci-numbers-in-finite-fields.md ├── geomancer-at-indigo.md ├── git-music.md ├── gits-push-url.md ├── global-game-jam-2012.md ├── global-game-jam-2014.md ├── global-game-jam-2015.md ├── implementing-a-typechecker-for-rcl-in-rust.md ├── llm-interactions.md ├── model-facts-not-your-problem-domain.md ├── neither-necessary-nor-sufficient.md ├── on-benchmarking.md ├── one-year-with-colemak.md ├── passphrase-entropy.md ├── please-put-units-in-names.md ├── the-small-bang-theory-in-san-francisco.md ├── the-task-monad-in-csharp.md ├── the-yaml-document-from-hell.md ├── working-on-a-virtualenv-without-magic.md ├── writing-a-path-tracer-in-rust-part-1.md ├── writing-a-path-tracer-in-rust-part-2-first-impressions.md ├── writing-a-path-tracer-in-rust-part-3-operators.md ├── writing-a-path-tracer-in-rust-part-4-tracing-rays.md ├── writing-a-path-tracer-in-rust-part-5-tonemapping.md ├── writing-a-path-tracer-in-rust-part-6-multithreading.md ├── writing-a-path-tracer-in-rust-part-7-conclusion.md ├── yaose-is-now-free-software.md └── zero-cost-abstractions.md ├── readme.md ├── src ├── Html.hs ├── Image.hs ├── Main.hs ├── Minification.hs ├── Post.hs ├── Template.hs └── Type.hs ├── templates ├── archive.html ├── contact.html ├── feed.xml ├── fonts.css ├── footer.html ├── head.html ├── index.html ├── math.css ├── page.css └── post.html └── tools ├── bazelsvg.py ├── grid.js ├── scale.html ├── stats.awk └── stats.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # Build output 2 | /.stack-work 3 | /dist 4 | /src/*.hi 5 | /src/*.o 6 | /blog 7 | 8 | # Cabal sandbox 9 | /.cabal-sandbox 10 | /cabal.sandbox.config 11 | 12 | # Editor files 13 | *.swp 14 | *.swo 15 | 16 | # Nix build result symlink 17 | /result 18 | 19 | # Image intermediate build products 20 | .ninja_log 21 | /images/compressed/*.br 22 | /images/compressed/*.gz 23 | 24 | # Fonts, original and processed 25 | /fonts/generated 26 | /fonts/original 27 | 28 | # Generated blog output 29 | /out 30 | /docindex/index.rendered.html 31 | 32 | # Python virtualenv 33 | /venv 34 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | blog: /usr/bin/fd . posts | entr blog 2 | http: python -m http.server 8888 --bind 0.0.0.0 --directory out 3 | -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruuda/blog/bab1f438a0057239b9e0ec41007a1ed8b8dbc4b6/assets/favicon.png -------------------------------------------------------------------------------- /assets/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /docindex/build.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2023 Ruud van Asseldonk 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License version 3. See 7 | # the licence file in the root of the repository. 8 | 9 | from datetime import datetime 10 | from typing import Dict, Iterable, List, NamedTuple, Tuple 11 | 12 | import json 13 | import toml 14 | 15 | 16 | class Repo(NamedTuple): 17 | title: str 18 | slug: str 19 | description: str 20 | stars: int 21 | 22 | 23 | def load_repos() -> Iterable[Repo]: 24 | with open("repos.toml", "r", encoding="utf-8") as f: 25 | for title, properties in toml.load(f).items(): 26 | yield Repo(title, **properties, stars=0) 27 | 28 | 29 | def load_stars() -> Tuple[datetime, Dict[str, int]]: 30 | with open("stars.json", "r", encoding="utf-8") as f: 31 | data = json.load(f) 32 | generated_at = datetime.fromisoformat(data["generated_at"]) 33 | assert generated_at.tzinfo is not None 34 | stars: Dict[str, int] = data["stars"] 35 | return generated_at, stars 36 | 37 | 38 | def render_project_card(repo: Repo) -> str: 39 | return f""" 40 | 48 | """ 49 | 50 | 51 | def render_template(variables: Dict[str, str]) -> str: 52 | with open("index.html", "r", encoding="utf-8") as f: 53 | output = f.read() 54 | for needle, replacement in variables.items(): 55 | output = output.replace("{{" + needle + "}}", replacement) 56 | 57 | return output 58 | 59 | 60 | def main() -> None: 61 | generated_at, stars = load_stars() 62 | repos = [repo._replace(stars=stars[repo.slug]) for repo in load_repos()] 63 | repos.sort(key=lambda r: r.stars, reverse=True) 64 | variables = { 65 | "generated_at": generated_at.strftime("%B %Y"), 66 | "projects": "".join(render_project_card(repo) for repo in repos), 67 | } 68 | print(render_template(variables)) 69 | 70 | 71 | if __name__ == "__main__": 72 | main() 73 | -------------------------------------------------------------------------------- /docindex/get_stars.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2023 Ruud van Asseldonk 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License version 3. See 7 | # the licence file in the root of the repository. 8 | 9 | from datetime import datetime, timezone 10 | from http.client import HTTPSConnection 11 | from typing import Dict 12 | 13 | import json 14 | import urllib.request 15 | 16 | from build import load_repos 17 | 18 | 19 | def count_stars(client: HTTPSConnection, owner: str, repo: str) -> int: 20 | n = 0 21 | page = 0 22 | 23 | while True: 24 | # There is a Link header with a link to the next and last page, but it 25 | # is annying to parse, we'll just iterate until we no longer get 30 26 | # results in the page. 27 | page += 1 28 | client.request( 29 | "GET", 30 | f"/repos/{owner}/{repo}/stargazers?per_page=30&page={page}", 31 | headers={ 32 | "Accept": "application/vnd.github+json", 33 | "User-Agent": "Ruud's Star Counting Script ", 34 | "X-GitHub-Api-Version": "2022-11-28", 35 | }, 36 | ) 37 | with client.getresponse() as response: 38 | assert response.status == 200 39 | data = json.load(response) 40 | n += len(data) 41 | if len(data) < 30: 42 | break 43 | 44 | return n 45 | 46 | 47 | def main() -> None: 48 | now = datetime.now(tz=timezone.utc) 49 | result: Dict[str, int] = {} 50 | 51 | timeout_seconds = 9.1 52 | client = HTTPSConnection("api.github.com", timeout=timeout_seconds) 53 | 54 | for repo in load_repos(): 55 | result[repo.slug] = count_stars(client, "ruuda", repo.slug) 56 | 57 | data = { 58 | "generated_at": now.isoformat(), 59 | "stars": result, 60 | } 61 | 62 | with open("stars.json", "w", encoding="utf-8") as f: 63 | json.dump(data, f, indent=2) 64 | 65 | 66 | if __name__ == "__main__": 67 | main() 68 | -------------------------------------------------------------------------------- /docindex/readme.md: -------------------------------------------------------------------------------- 1 | # Documentation Index 2 | 3 | This directory contains the script and template that generate the index page for 4 | . That documentation site is otherwise not so related to 5 | my main site and the blog generator, but I wanted to put the code somewhere, so 6 | I might as well put it here. 7 | 8 | * `build.py` writes the html to stdout, based on `repos.toml` and `stars.json`. 9 | * `get_stars.py` refreshes `stars.json` based on `repos.toml`. 10 | 11 | Both scripts have no dependencies other than Python ≥3.11. 12 | 13 | ## License 14 | 15 | The scripts are licensed under the GPL v3 like all of the other code in this 16 | repository. The index page stylesheet is licensed Apache 2.0 like [the 17 | Kilsbergen theme](https://github.com/ruuda/kilsbergen) itself. 18 | -------------------------------------------------------------------------------- /docindex/repos.toml: -------------------------------------------------------------------------------- 1 | # All my repositories that have a documentation site. 2 | 3 | [Hanson] 4 | slug = "hanson" 5 | description = "Self-hosted prediction market app." 6 | 7 | [Musium] 8 | slug = "musium" 9 | description = "Music playback daemon with web-based library browser." 10 | 11 | [Noblit] 12 | slug = "noblit" 13 | description = "An immutable append-only database." 14 | 15 | [Pris] 16 | slug = "pris" 17 | description = "A language for designing slides." 18 | 19 | [RCL] 20 | slug = "rcl" 21 | description = "A reasonable configuration language." 22 | 23 | [Squiller] 24 | slug = "squiller" 25 | description = "Generate boilerplate from annotated SQL queries." 26 | 27 | [Tako] 28 | slug = "tako" 29 | description = "Updater for single files." 30 | -------------------------------------------------------------------------------- /docindex/stars.json: -------------------------------------------------------------------------------- 1 | { 2 | "generated_at": "2024-06-15T20:10:53.655663+00:00", 3 | "stars": { 4 | "hanson": 5, 5 | "musium": 74, 6 | "noblit": 27, 7 | "pris": 114, 8 | "rcl": 140, 9 | "squiller": 3, 10 | "tako": 7 11 | } 12 | } -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1725407940, 6 | "narHash": "sha256-tiN5Rlg/jiY0tyky+soJZoRzLKbPyIdlQ77xVgREDNM=", 7 | "owner": "NixOS", 8 | "repo": "nixpkgs", 9 | "rev": "6f6c45b5134a8ee2e465164811e451dcb5ad86e3", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "id": "nixpkgs", 14 | "ref": "nixos-24.05", 15 | "type": "indirect" 16 | } 17 | }, 18 | "root": { 19 | "inputs": { 20 | "nixpkgs": "nixpkgs" 21 | } 22 | } 23 | }, 24 | "root": "root", 25 | "version": 7 26 | } 27 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Blog"; 3 | 4 | inputs.nixpkgs.url = "nixpkgs/nixos-24.05"; 5 | 6 | outputs = { self, nixpkgs }: 7 | let 8 | system = "x86_64-linux"; 9 | pkgs = import nixpkgs { inherit system; }; 10 | 11 | ghc = pkgs.ghc.withPackages (ps: [ 12 | ps.async 13 | ps.containers 14 | ps.pandoc 15 | ps.tagsoup 16 | ps.text 17 | ]); 18 | 19 | # Build the static site generator directly and make it part of the 20 | # development environment. 21 | blog = pkgs.stdenv.mkDerivation rec { 22 | name = "${pname}-${version}"; 23 | pname = "blog"; 24 | version = "3.1.0"; 25 | 26 | src = ./src; 27 | 28 | # For the run time options, use 4 threads (-N4), and use a heap of 29 | # 256 MiB (-H). These settings were found to be optimal by running 30 | # ghc-gc-tune. 31 | ghcOptions = [ 32 | "-Wall" 33 | "-fwarn-tabs" 34 | "-O3" 35 | "-threaded" 36 | "-rtsopts \"-with-rtsopts=-N4 -A8388608 -H268435456\"" 37 | ]; 38 | 39 | buildPhase = '' 40 | ${ghc}/bin/ghc -o blog -outputdir . "$ghcOptions" $src/*.hs 41 | ''; 42 | installPhase = '' 43 | mkdir -p $out/bin 44 | cp blog $out/bin 45 | ''; 46 | }; 47 | in 48 | { 49 | packages."${system}".default = blog; 50 | 51 | devShells."${system}".default = pkgs.mkShell { 52 | name = "blog"; 53 | nativeBuildInputs = [ 54 | blog 55 | # Include GHC so we can still hack on the generator without having 56 | # to run "nix build" all the time. 57 | ghc 58 | # And the runtime dependencies of the generator and utilities. 59 | (pkgs.python39.withPackages (ps: [ 60 | ps.brotli 61 | ps.fontforge 62 | ps.fonttools 63 | ])) 64 | pkgs.brotli 65 | pkgs.guetzli 66 | pkgs.hivemind 67 | pkgs.m4 68 | pkgs.mozjpeg 69 | pkgs.optipng 70 | pkgs.scour 71 | pkgs.zopfli 72 | ]; 73 | }; 74 | }; 75 | } 76 | -------------------------------------------------------------------------------- /fonts/extra/math-italic.sfd: -------------------------------------------------------------------------------- 1 | SplineFontDB: 3.0 2 | FontName: CallunaSansMath-Italic 3 | FullName: Calluna Sans Math 4 | FamilyName: Calluna Sans Math 5 | Weight: Regular 6 | Copyright: Copyright 2017 Ruud van Asseldonk 7 | Version: 1 8 | ItalicAngle: 0 9 | UnderlinePosition: -100 10 | UnderlineWidth: 50 11 | Ascent: 750 12 | Descent: 250 13 | InvalidEm: 0 14 | sfntRevision: 0x00010000 15 | LayerCount: 2 16 | Layer: 0 0 "Back" 1 17 | Layer: 1 0 "Fore" 0 18 | XUID: [1021 203 -1014936935 6831600] 19 | StyleMap: 0x0000 20 | FSType: 8 21 | OS2Version: 2 22 | OS2_WeightWidthSlopeOnly: 0 23 | OS2_UseTypoMetrics: 0 24 | CreationTime: 1292841550 25 | ModificationTime: 1498933262 26 | PfmFamily: 17 27 | TTFWeight: 400 28 | TTFWidth: 5 29 | LineGap: 0 30 | VLineGap: 0 31 | Panose: 2 0 0 0 0 0 0 0 0 0 32 | OS2TypoAscent: 750 33 | OS2TypoAOffset: 0 34 | OS2TypoDescent: -250 35 | OS2TypoDOffset: 0 36 | OS2TypoLinegap: 200 37 | OS2WinAscent: 940 38 | OS2WinAOffset: 0 39 | OS2WinDescent: 260 40 | OS2WinDOffset: 0 41 | HheadAscent: 940 42 | HheadAOffset: 0 43 | HheadDescent: -260 44 | HheadDOffset: 0 45 | OS2SubXSize: 700 46 | OS2SubYSize: 650 47 | OS2SubXOff: 0 48 | OS2SubYOff: 140 49 | OS2SupXSize: 700 50 | OS2SupYSize: 650 51 | OS2SupXOff: 0 52 | OS2SupYOff: 477 53 | OS2StrikeYSize: 50 54 | OS2StrikeYPos: 250 55 | OS2CapHeight: 667 56 | OS2XHeight: 450 57 | OS2Vendor: 'PfEd' 58 | OS2CodePages: 2000009b.00000000 59 | OS2UnicodeRanges: a000002f.5000206b.00000000.00000000 60 | MarkAttachClasses: 1 61 | DEI: 91125 62 | LangName: 1033 "" "" "" "" "" "" "" "" "" "Ruud van Asseldonk" "" "" "https://ruudvanasseldonk.com" 63 | Encoding: Custom 64 | UnicodeInterp: none 65 | NameList: AGL For New Fonts 66 | DisplaySize: -96 67 | AntiAlias: 1 68 | FitToEm: 0 69 | WinInfo: 825 25 10 70 | BeginPrivate: 6 71 | BlueValues 23 [-12 0 450 462 667 679] 72 | OtherBlues 11 [-237 -225] 73 | StdHW 4 [68] 74 | StdVW 4 [78] 75 | StemSnapH 10 [55 68 71] 76 | StemSnapV 13 [67 78 82 88] 77 | EndPrivate 78 | TeXData: 1 0 0 1048576 524288 349525 0 1048576 349525 783286 444596 497025 792723 393216 433062 380633 303038 157286 324010 404750 52429 2506097 1059062 262144 79 | BeginChars: 66290 1 80 | 81 | StartChar: sigma 82 | Encoding: 963 963 0 83 | Width: 504 84 | Flags: HMW 85 | HStem: -12 65<173.5 260.5 173.5 282.5> 397 65<234 314> 86 | LayerCount: 2 87 | Fore 88 | SplineSet 89 | 110 182 m 0 90 | 110 107 139 53 208 53 c 0 91 | 313 53 377 173 377 275 c 0 92 | 377 344 348 397 280 397 c 0 93 | 188 397 110 304 110 182 c 0 94 | 418 400 m 1 95 | 444 366 457 322 457 278 c 0 96 | 457 160 363 -12 202 -12 c 0 97 | 86 -12 30 79 30 179 c 0 98 | 30 317 137 462 284 462 c 2 99 | 518 462 l 1 100 | 504 400 l 1 101 | 422 400 l 0 102 | 418 400 l 1 103 | EndSplineSet 104 | Validated: 1 105 | EndChar 106 | EndChars 107 | EndSplineFont 108 | -------------------------------------------------------------------------------- /fonts/extra/math-upright.sfd: -------------------------------------------------------------------------------- 1 | SplineFontDB: 3.2 2 | FontName: CallunaSansMath-Regular 3 | FullName: Calluna Sans Math 4 | FamilyName: Calluna Sans Math 5 | Weight: Regular 6 | Copyright: Copyright 2016 Ruud van Asseldonk 7 | Version: 1 8 | ItalicAngle: 0 9 | UnderlinePosition: -100 10 | UnderlineWidth: 50 11 | Ascent: 750 12 | Descent: 250 13 | InvalidEm: 0 14 | sfntRevision: 0x00010000 15 | LayerCount: 2 16 | Layer: 0 0 "Back" 1 17 | Layer: 1 0 "Fore" 0 18 | XUID: [1021 203 -1014936935 6831600] 19 | StyleMap: 0x0000 20 | FSType: 8 21 | OS2Version: 2 22 | OS2_WeightWidthSlopeOnly: 0 23 | OS2_UseTypoMetrics: 0 24 | CreationTime: 1292841550 25 | ModificationTime: 1689719331 26 | PfmFamily: 17 27 | TTFWeight: 400 28 | TTFWidth: 5 29 | LineGap: 0 30 | VLineGap: 0 31 | Panose: 2 0 0 0 0 0 0 0 0 0 32 | OS2TypoAscent: 750 33 | OS2TypoAOffset: 0 34 | OS2TypoDescent: -250 35 | OS2TypoDOffset: 0 36 | OS2TypoLinegap: 200 37 | OS2WinAscent: 940 38 | OS2WinAOffset: 0 39 | OS2WinDescent: 260 40 | OS2WinDOffset: 0 41 | HheadAscent: 940 42 | HheadAOffset: 0 43 | HheadDescent: -260 44 | HheadDOffset: 0 45 | OS2SubXSize: 700 46 | OS2SubYSize: 650 47 | OS2SubXOff: 0 48 | OS2SubYOff: 140 49 | OS2SupXSize: 700 50 | OS2SupYSize: 650 51 | OS2SupXOff: 0 52 | OS2SupYOff: 477 53 | OS2StrikeYSize: 50 54 | OS2StrikeYPos: 250 55 | OS2CapHeight: 667 56 | OS2XHeight: 450 57 | OS2Vendor: 'PfEd' 58 | OS2CodePages: 2000009b.00000000 59 | OS2UnicodeRanges: a000002f.5000206b.00000000.00000000 60 | MarkAttachClasses: 1 61 | DEI: 91125 62 | LangName: 1033 "" "" "" "" "" "" "" "" "" "Ruud van Asseldonk" "" "" "https://ruudvanasseldonk.com" 63 | Encoding: Custom 64 | UnicodeInterp: none 65 | NameList: AGL For New Fonts 66 | DisplaySize: -72 67 | AntiAlias: 1 68 | FitToEm: 0 69 | WinInfo: 8652 14 8 70 | BeginPrivate: 6 71 | BlueValues 23 [-12 0 450 462 667 679] 72 | OtherBlues 11 [-237 -225] 73 | StdHW 4 [68] 74 | StdVW 4 [78] 75 | StemSnapH 10 [55 68 71] 76 | StemSnapV 13 [67 78 82 88] 77 | EndPrivate 78 | TeXData: 1 0 0 1048576 524288 349525 0 1048576 349525 783286 444596 497025 792723 393216 433062 380633 303038 157286 324010 404750 52429 2506097 1059062 262144 79 | BeginChars: 65589 6 80 | 81 | StartChar: u1D53D 82 | Encoding: 133 120125 0 83 | Width: 542 84 | Flags: HMW 85 | HStem: 0 21G<98 98 98 180> 299 73<180 419 180 419> 598 69<180 482 180 180> 86 | VStem: 98 82<0 299 372 598> 87 | LayerCount: 2 88 | Fore 89 | SplineSet 90 | 98 0 m 1 91 | 98 667 l 1 92 | 532 667 l 1 93 | 532 598 l 1 94 | 290 598 l 1 95 | 290 372 l 1 96 | 469 372 l 1 97 | 469 299 l 1 98 | 290 299 l 1 99 | 290 0 l 5 100 | 98 0 l 1 101 | 229 70 m 5 102 | 229 598 l 1 103 | 180 598 l 1 104 | 180 70 l 1 105 | 229 70 l 5 106 | EndSplineSet 107 | Validated: 1 108 | EndChar 109 | 110 | StartChar: uni2309 111 | Encoding: 8969 8969 1 112 | Width: 287 113 | Flags: HW 114 | HStem: -155 55<19 137 19 202 19 137> 612 55<19 137 19 202> 115 | VStem: 137 65<-100 612 612 612> 116 | LayerCount: 2 117 | Fore 118 | SplineSet 119 | 136 -157 m 1 120 | 137 612 l 1 121 | 19 612 l 1 122 | 19 667 l 1 123 | 202 667 l 1 124 | 202 -155 l 1 125 | 136 -157 l 1 126 | EndSplineSet 127 | EndChar 128 | 129 | StartChar: uni2308 130 | Encoding: 8968 8968 2 131 | Width: 287 132 | Flags: HW 133 | HStem: -155 55<150 268 150 268> 612 55<150 268 150 150> 134 | VStem: 85 65<-100 612 -100 667 -100 667> 135 | LayerCount: 2 136 | Fore 137 | SplineSet 138 | 150 -155 m 1 139 | 85 -155 l 1 140 | 85 667 l 1 141 | 268 667 l 1 142 | 268 612 l 1 143 | 150 612 l 1 144 | 150 -155 l 1 145 | EndSplineSet 146 | EndChar 147 | 148 | StartChar: uni230A 149 | Encoding: 8970 8970 3 150 | Width: 287 151 | Flags: HW 152 | HStem: -155 55<150 268 150 268> 612 55<150 268 150 150> 153 | VStem: 85 65<-100 612 -100 667 -100 667> 154 | LayerCount: 2 155 | Fore 156 | SplineSet 157 | 150 -100 m 1 158 | 268 -100 l 1 159 | 268 -155 l 1 160 | 85 -155 l 1 161 | 85 667 l 1 162 | 148 667 l 1 163 | 150 -100 l 1 164 | EndSplineSet 165 | EndChar 166 | 167 | StartChar: uni230B 168 | Encoding: 8971 8971 4 169 | Width: 287 170 | Flags: HW 171 | HStem: -155 55<19 137 19 202 19 137> 612 55<19 137 19 202> 172 | VStem: 137 65<-100 612 612 612> 173 | LayerCount: 2 174 | Fore 175 | SplineSet 176 | 136 667 m 1 177 | 202 667 l 1 178 | 202 -155 l 1 179 | 19 -155 l 1 180 | 19 -100 l 1 181 | 137 -100 l 1 182 | 136 667 l 1 183 | EndSplineSet 184 | EndChar 185 | 186 | StartChar: qed 187 | Encoding: 8718 8718 5 188 | Width: 604 189 | Flags: HW 190 | HStem: 0 65<146 461> 386 64<146 461> 191 | VStem: 77 69<65 386> 461 66<65 386> 192 | LayerCount: 2 193 | Fore 194 | SplineSet 195 | 146 65 m 1 196 | 461 65 l 1 197 | 461 386 l 1 198 | 146 386 l 5 199 | 146 65 l 1 200 | 77 0 m 1 201 | 77 450 l 1 202 | 527 450 l 1 203 | 527 0 l 1 204 | 77 0 l 1 205 | EndSplineSet 206 | EndChar 207 | EndChars 208 | EndSplineFont 209 | -------------------------------------------------------------------------------- /fonts/generate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2016 Ruud van Asseldonk 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License version 3. See 7 | # the licence file in the root of the repository. 8 | 9 | import fontforge 10 | import fontTools.subset as fonttools 11 | import os 12 | 13 | 14 | # Note: roundtripping an otf font through FontForge (with open and generate) is 15 | # not a lossless process, and it only appears to reach a fixed point after the 16 | # third roundtrip. One difference after roundtripping is that FontForge adds an 17 | # FFTM table with timestamps of last modification. It also adds a GDEF table. 18 | # Furthermore, it modifies the programs and other values in the CCF table. The 19 | # new values are a different representation of the same data. 20 | 21 | 22 | # Removes some unnecessary data from an otf font. 23 | def prune_font(fontfile, out_dir): 24 | font = fonttools.load_font(fontfile, fonttools.Options()) 25 | 26 | # Roundtripping a font trough FontForge adds a GDEF table. This table is not 27 | # present in the original version of Calluna, so remove it. It is present in 28 | # Inconsolata, but it does not appear to do any harm to remove it, apart 29 | # from reducing the file size. 30 | if 'GDEF' in font: 31 | del font['GDEF'] 32 | 33 | font.save(os.path.join(out_dir, os.path.basename(fontfile))) 34 | font.close() 35 | 36 | 37 | def main(): 38 | os.makedirs('generated/', exist_ok = True) 39 | 40 | # Merge math-upright (which contains U+1D53D, a double-struck F) 41 | # into Calluna Sans. 42 | calluna_sans = fontforge.open('original/calluna-sans.otf') 43 | calluna_sans.mergeFonts('extra/math-upright.sfd') 44 | calluna_sans.generate('generated/calluna-sans.otf', flags = 'opentype') 45 | calluna_sans.close() 46 | 47 | prune_font('generated/calluna-sans.otf', 'generated/') 48 | 49 | # Merge math-italic (which contains a sigma) into Calluna Sans Italic. 50 | calluna_sansi = fontforge.open('original/calluna-sans-italic.otf') 51 | calluna_sansi.mergeFonts('extra/math-italic.sfd') 52 | calluna_sansi.generate('generated/calluna-sans-italic.otf', flags = 'opentype') 53 | calluna_sansi.close() 54 | 55 | prune_font('generated/calluna-sans-italic.otf', 'generated/') 56 | 57 | # Just copy over the other fonts, prune them in the process. 58 | prune_font('original/calluna-bold.otf', 'generated/') 59 | prune_font('original/calluna-italic.otf', 'generated/') 60 | prune_font('original/calluna.otf', 'generated/') 61 | prune_font('original/calluna-sans-bold.otf', 'generated/') 62 | prune_font('original/inconsolata.otf', 'generated/') 63 | 64 | 65 | main() 66 | -------------------------------------------------------------------------------- /fonts/readme.md: -------------------------------------------------------------------------------- 1 | Fonts 2 | ===== 3 | 4 | I use the [Calluna][calluna] family throughout the site. It is a beautiful 5 | humanist family designed by Jos Buivenga and issued by [Exljbris][exljbris]. 6 | The regular sans and serif are available free of charge, but otherwise it is a 7 | commercial family, so for obvious reasons the font files are not included in 8 | this repository. 9 | 10 | As monospace font I use [Inconsolata][incons] by Raph Levien. It is inspired 11 | by my all-time favorite coding font Consolas, and it works very well in 12 | combination with Calluna. Inconsolata is a free font licensed under the 13 | [SIL Open Font License][ofl]. 14 | 15 | [calluna]: http://www.exljbris.com/calluna.html 16 | [exljbris]: http://www.exljbris.com 17 | [incons]: http://levien.com/type/myfonts/inconsolata.html 18 | [ofl]: http://scripts.sil.org/OFL 19 | 20 | Extra Glyphs 21 | ------------ 22 | Calluna does not have glyphs for all the characters that I use on my blog. The 23 | following code points do not have a glyph: 24 | 25 | - U+03C6 Greek small letter phi 26 | - U+03C8 Greek small letter psi 27 | - U+220E End of Proof 28 | - U+2261 Identical to 29 | - U+1D53D Mathematical double-struck capital F 30 | 31 | Browsers fall back to the next font for characters not supported by a font, but 32 | the fallback font used by Chrome on Android does not have the double-struck F, 33 | so I designed my own. For the other glyphs a fallback is acceptable. Perhaps in 34 | the future I’ll roll my own for them too. The FontForge sources for custom 35 | glyphs are in the extra directory. 36 | 37 | The script generate.py adds the extra glyphs to the fonts and puts the result in 38 | the generated directory. I don’t mean to imply that the extra glyphs were part 39 | of the original fonts. The reason for doing this is technical: for a font with 40 | a single glyph the metadata overhead is considerable, and the extra font would 41 | unneccessarily complicate my stylesheets and html. 42 | 43 | Subsetting 44 | ---------- 45 | The script subset.py is invoked by the main blog generator to subset the fonts 46 | specifically for every page (see also src/Type.hs). This allows me to do very 47 | aggressive subsetting and keep the file size low. 48 | -------------------------------------------------------------------------------- /fonts/subset.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2015 Ruud van Asseldonk 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License version 3. See 7 | # the licence file in the root of the repository. 8 | 9 | from fontTools.subset import Options, Subsetter, load_font, save_font 10 | from sys import stdin 11 | 12 | # We expect to be using the pinned version. If it differs, we are probably not 13 | # running from the Nix profile. 14 | import fontTools 15 | 16 | assert fontTools.version == "4.51.0", fontTools.version 17 | 18 | 19 | # Removes format 12 cmap tables if they are not required. A format 4 table is 20 | # always included, but this format can only encode code points in the Basic 21 | # Multilingual Plane (BMP). One code point that I use (U+1D53D, a double-struck 22 | # F), is not in this plane, so if that code point is present, the format 12 23 | # table cannot be removed. In other cases it is a completely redundant copy of 24 | # the format 4 table, so strip it to save space. 25 | def prune_cmaps(font): 26 | tables = font["cmap"].tables 27 | min_len = min(len(table.cmap) for table in tables) 28 | max_len = max(len(table.cmap) for table in tables) 29 | 30 | # Type 12 tables should be a superset of the type 4 tables, so if there is 31 | # no extra glyph in the type 12 table, the lengths should match. 32 | if min_len == max_len: 33 | tables[:] = [table for table in tables if table.format != 12] 34 | 35 | 36 | def subset(fontfile, outfile_basename, glyphs): 37 | options = Options() 38 | 39 | # Disable some default-enabled features that we do not use. 40 | options.layout_features.remove("frac") 41 | options.layout_features.remove("numr") 42 | options.layout_features.remove("dnom") 43 | 44 | # There are some dlig glyphs that we want to include. 45 | options.layout_features.append("dlig") 46 | 47 | # Do not include extra glypths that may be reachable through OpenType 48 | # features, I specify all the glyphs I want, and nothing more. 49 | options.layout_closure = False 50 | 51 | # Same for small caps, it needs to be enabled explicitly. Luckily, only the 52 | # glyphs in the list get included, no extra ones. 53 | if any(g.endswith(".smcp") for g in glyphs): 54 | options.layout_features.append("smcp") 55 | options.layout_features.append("c2sc") 56 | 57 | # Fonts that went through the FontForge roundtrip will have subroutinized 58 | # programs in the CFF table. This presumably reduces file size for full 59 | # fonts, but on subsetted fonts it hurts file size and compressability, so 60 | # desubroutinize. 61 | options.desubroutinize = True 62 | 63 | # Do not keep the scripts. Older versions of fonttools didn't seem to create 64 | # a "DFLT" or "latn" script tag in all cases. However, if we remove "latn", 65 | # then many glyphs (ligatures, smcp) don't get included correctly any more. 66 | options.layout_scripts = ["latn"] 67 | 68 | # Preserve only name id 1 and 2, the name of the font and the style. Older 69 | # versions of fonttools preserved only those, newer versions preserve also 70 | # other ids which contain things like a version number and makeotf writer 71 | # version, which are pointless to distribute to web clients. (In fact all 72 | # the name ids are, but let's keep them to have some info about what the 73 | # font is remaining.) 74 | options.name_IDs = [1, 2] 75 | 76 | # The "FontForge Time Stamp Table" is useless in our output, delete it. 77 | options.drop_tables.append("FFTM") 78 | 79 | font = load_font(fontfile, options) 80 | 81 | subsetter = Subsetter(options=options) 82 | subsetter.populate(glyphs=glyphs) 83 | subsetter.subset(font) 84 | 85 | prune_cmaps(font) 86 | 87 | options.flavor = "woff" 88 | save_font(font, outfile_basename + ".woff", options) 89 | 90 | options.flavor = "woff2" 91 | save_font(font, outfile_basename + ".woff2", options) 92 | 93 | font.close() 94 | 95 | 96 | # Reads three lines from stdin at a time: the source font file, the destination 97 | # font file basename, and a space-separated list of glyph names to include. 98 | def main(): 99 | while True: 100 | fontfile = stdin.readline() 101 | outfile_basename = stdin.readline() 102 | glyphs = stdin.readline() 103 | 104 | if not fontfile or not outfile_basename or not glyphs: 105 | break 106 | 107 | glyph_names = glyphs.strip().split(" ") 108 | subset(fontfile.strip(), outfile_basename.strip(), glyph_names) 109 | 110 | 111 | main() 112 | -------------------------------------------------------------------------------- /images/build.ninja: -------------------------------------------------------------------------------- 1 | rule scour 2 | description = Minifying $out 3 | command = scour --enable-id-stripping --set-precision=3 --enable-viewboxing --indent=none --no-line-breaks -i $in -o $out 4 | 5 | # This file has been compressed through different means. 6 | # build compressed/build.svg: scour original/build.svg 7 | 8 | build compressed/lattice.svg: scour original/lattice.svg 9 | build compressed/rectangles.svg: scour original/rectangles.svg 10 | build compressed/subtypes.svg: scour original/subtypes.svg 11 | -------------------------------------------------------------------------------- /images/compress.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Compresses the resized images to the format that will be served. Because the 4 | # effect of jpeg compression depends a lot on the image, the compression 5 | # settings have been tweaked for every image individually. 6 | 7 | # I have a few images where jpeg performs poorly and png performs really well. 8 | # Sometimes the jpeg is slightly smaller than the png at a quality setting 9 | # where the artifacts become unnoticeable, sometimes png actually outperforms 10 | # jpeg. 11 | 12 | # Fail as soon as any command fails. 13 | set -e 14 | 15 | # Verify that we really have Mozjpeg and not regular libjpeg-turbo, as they 16 | # share the same binary name. Mozjpeg prints its version to stderr. 17 | cjpeg -version 2>&1 | grep mozjpeg 18 | 19 | # TODO: With mozjpeg 3.1, images were much smaller at the same quality settings 20 | # than with later versions. Figure out which settings compress to the same size 21 | # (or quality!). 22 | mozjpeg='cjpeg -quality' 23 | 24 | mkdir -p compressed 25 | 26 | $mozjpeg 88.0 resized/geomancer-soldier-under-attack-by-mage.png > compressed/geomancer-soldier-under-attack-by-mage.jpg 27 | $mozjpeg 88.0 resized/geomancer-move-mage.png > compressed/geomancer-move-mage.jpg 28 | $mozjpeg 88.0 resized/geomancer-overview.png > compressed/geomancer-overview.jpg 29 | $mozjpeg 94.5 resized/robigo-luculenta.png > compressed/robigo-luculenta.jpg 30 | $mozjpeg 85.0 resized/the-small-bang-theory.png > compressed/the-small-bang-theory.jpg 31 | 32 | guetzli --quality 93 resized/richys-groceries.png compressed/richys-groceries.jpg 33 | 34 | # For this file, at the quality setting where the jpeg artifacts become mostly 35 | # unnoticeable, the jpeg is actually larger than the png, so opt for the png. 36 | cp resized/tristar-cyan.png compressed/tristar-cyan.png 37 | 38 | # Guetzli experiments 39 | # ------------------- 40 | # Conclusion: apart from one image, Guetzli is universally worse than Mozjpeg 41 | # for my use case. The main problem is that although it tends to produce less 42 | # artifacts, it produces blocky gradients, which are much worse. My images do 43 | # contain lots of gradients and relatively little high-frequency detail. 44 | 45 | # geomancer-soldier-under-attack-by-mage.png 46 | # Compressor Quality Size (KiB) Result 47 | # Guetzli 90.0 191 almost acceptable (less artifacts than mozjpeg, but blocky gradients) 48 | # Guetzli 87.0 161 bad (blocky gradients, minor artifacts) 49 | # Guetzli 84.0 146 bad (very blocky gradients, artifacts) 50 | # Mozjpeg 88.0 158 acceptable (minor artifacts along edges) 51 | 52 | # geomancer-move-mage.png 53 | # Compressor Quality Size (KiB) Result 54 | # Guetzli 90.0 205 almost acceptable (blocky gradients) 55 | # Guetzli 87.0 175 almost acceptable (artifacts along edges rather than blur, blocky gradients) 56 | # Mozjpeg 88.0 176 acceptable (minor artifacts along edges) 57 | 58 | # geomancer-overview.png 59 | # Compressor Quality Size (KiB) Result 60 | # Guetzli 90.0 107 acceptable 61 | # Guetzli 87.0 84 acceptable, but slightly worse than mozjpeg in edge artifacts 62 | # Guetzli 86.0 82 almost acceptable (still noticeable artifacts along edges) 63 | # Guetzli 85.0 78 bad (artifacts along edges) 64 | # Mozjpeg 88.0 84 acceptable (artifacts in high-detail regions, but not along edges) 65 | 66 | # robigo-luculenta.png 67 | # Compressor Quality Size (KiB) Result 68 | # Guetzli 95.0 192 good 69 | # Guetzli 93.0 130 good 70 | # Guetzli 92.0 118 almost acceptable (still blocky gradients) 71 | # Guetzli 91.0 111 almost acceptable (still blocky gradients) 72 | # Guetzli 90.0 106 bad (gradients turn blocky) 73 | # Mozjpeg 94.5 126 good 74 | 75 | # richys-groceries.png 76 | # Compressor Quality Size (KiB) Result 77 | # Guetzli 99.0 400 good 78 | # Guetzli 95.0 241 good 79 | # Guetzli 93.0 187 good 80 | # Guetzli 92.0 168 barely acceptable (shows minor artifacts) 81 | # Guetzli 91.0 130 shows artifacts 82 | # Guetzli 90.0 124 shows artifacts 83 | # Mozjpeg 99.0 199 acceptable (parts blurry, but that's ok) 84 | 85 | # the-small-bang-theory.png 86 | # Compressor Quality Size (KiB) Result 87 | # Guetzli 85.0 57 acceptable (minor artifacts) 88 | # Guetzli 84.0 55 bad (noticeable artifacts around text) 89 | # Mozjpeg 85.0 33 acceptable (minor artifacts) 90 | 91 | # tristar-cyan.png 92 | # Compressor Quality Size (KiB) Result 93 | # Guetzli 90.0 150 almost acceptable (artifacts) 94 | # Guetzli 84.0 124 bad (artifacts) 95 | # Mozjpeg 99.0 278 good 96 | # Mozjpeg 90.0 116 bad (artifacts) 97 | # Mozjpeg 89.5 83 bad (artifacts) 98 | # Mozjpeg 89.0 79 bad (artifacts) 99 | # Mozjpeg 80.0 57 terrible (artifacts) 100 | # None (png) 108 perfect 101 | -------------------------------------------------------------------------------- /images/compressed/geomancer-move-mage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruuda/blog/bab1f438a0057239b9e0ec41007a1ed8b8dbc4b6/images/compressed/geomancer-move-mage.jpg -------------------------------------------------------------------------------- /images/compressed/geomancer-overview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruuda/blog/bab1f438a0057239b9e0ec41007a1ed8b8dbc4b6/images/compressed/geomancer-overview.jpg -------------------------------------------------------------------------------- /images/compressed/geomancer-soldier-under-attack-by-mage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruuda/blog/bab1f438a0057239b9e0ec41007a1ed8b8dbc4b6/images/compressed/geomancer-soldier-under-attack-by-mage.jpg -------------------------------------------------------------------------------- /images/compressed/ill-build-my-own-configuration-language.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruuda/blog/bab1f438a0057239b9e0ec41007a1ed8b8dbc4b6/images/compressed/ill-build-my-own-configuration-language.png -------------------------------------------------------------------------------- /images/compressed/lattice.svg: -------------------------------------------------------------------------------- 1 | 2 | AnyList[Any]IntBoolStringNullList[Int]List[Bool]List[Void]Void 5 | -------------------------------------------------------------------------------- /images/compressed/rectangles.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /images/compressed/richys-groceries.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruuda/blog/bab1f438a0057239b9e0ec41007a1ed8b8dbc4b6/images/compressed/richys-groceries.jpg -------------------------------------------------------------------------------- /images/compressed/robigo-luculenta.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruuda/blog/bab1f438a0057239b9e0ec41007a1ed8b8dbc4b6/images/compressed/robigo-luculenta.jpg -------------------------------------------------------------------------------- /images/compressed/subtypes.svg: -------------------------------------------------------------------------------- 1 | 2 | Well-typedInconclusiveStatic errorUTUTUT 6 | -------------------------------------------------------------------------------- /images/compressed/the-small-bang-theory.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruuda/blog/bab1f438a0057239b9e0ec41007a1ed8b8dbc4b6/images/compressed/the-small-bang-theory.jpg -------------------------------------------------------------------------------- /images/compressed/tristar-cyan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruuda/blog/bab1f438a0057239b9e0ec41007a1ed8b8dbc4b6/images/compressed/tristar-cyan.png -------------------------------------------------------------------------------- /images/crush.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Before including a new image in the repository, crush it with this script. 4 | # Usage: 5 | # 6 | # $ ./crush.sh image.png 7 | # 8 | # This will overwrite image.png with the crushed version. 9 | 10 | # First apply OptiPNG in best quality but terribly slow mode. This is only run 11 | # once, so it is fine if it takes a few mintues to run. 12 | optipng -o7 $1 13 | 14 | # PNGOut can reduce the file size even further in some cases. Most of the 15 | # improvement comes from a custom deflator though, which will be outperformed 16 | # by ZopfliPNG. Still, any byte saved is a win. 17 | pngout $1 18 | 19 | # Finally, re-compress the IDAT stream with Zopfli for maximum compression 20 | # ratio. Trade slower compression speed for a better ratio here too. 21 | zopflipng -y --iterations=137 $1 $1 22 | -------------------------------------------------------------------------------- /images/original/geomancer-move-mage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruuda/blog/bab1f438a0057239b9e0ec41007a1ed8b8dbc4b6/images/original/geomancer-move-mage.png -------------------------------------------------------------------------------- /images/original/geomancer-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruuda/blog/bab1f438a0057239b9e0ec41007a1ed8b8dbc4b6/images/original/geomancer-overview.png -------------------------------------------------------------------------------- /images/original/geomancer-soldier-under-attack-by-mage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruuda/blog/bab1f438a0057239b9e0ec41007a1ed8b8dbc4b6/images/original/geomancer-soldier-under-attack-by-mage.png -------------------------------------------------------------------------------- /images/original/ill-build-my-own-configuration-language.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruuda/blog/bab1f438a0057239b9e0ec41007a1ed8b8dbc4b6/images/original/ill-build-my-own-configuration-language.png -------------------------------------------------------------------------------- /images/original/lattice.svg: -------------------------------------------------------------------------------- 1 | 2 | 9 | 14 | Any 15 | List[Any] 16 | Int 17 | Bool 18 | String 19 | Null 20 | List[Int] 21 | List[Bool] 22 | List[Void] 23 | Void 24 | 27 | 30 | 33 | 36 | 39 | 42 | 45 | 48 | 51 | 54 | 57 | 60 | 63 | 66 | 67 | -------------------------------------------------------------------------------- /images/original/rectangles.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/original/richys-groceries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruuda/blog/bab1f438a0057239b9e0ec41007a1ed8b8dbc4b6/images/original/richys-groceries.png -------------------------------------------------------------------------------- /images/original/robigo-luculenta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruuda/blog/bab1f438a0057239b9e0ec41007a1ed8b8dbc4b6/images/original/robigo-luculenta.png -------------------------------------------------------------------------------- /images/original/subtypes.svg: -------------------------------------------------------------------------------- 1 | 2 | 9 | 15 | Well-typed 16 | Inconclusive 17 | Static error 18 | 19 | U 20 | T 21 | 22 | 23 | 24 | U 25 | T 26 | 27 | 28 | 29 | U 30 | T 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /images/original/the-small-bang-theory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruuda/blog/bab1f438a0057239b9e0ec41007a1ed8b8dbc4b6/images/original/the-small-bang-theory.png -------------------------------------------------------------------------------- /images/original/tristar-cyan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruuda/blog/bab1f438a0057239b9e0ec41007a1ed8b8dbc4b6/images/original/tristar-cyan.png -------------------------------------------------------------------------------- /images/resize.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Resize an image to be 1280 pixels wide. Keep the same aspect ratio. 4 | # Usage: 5 | # 6 | # $ ./resize.sh image.png image-1280.png 7 | 8 | # Resize in the linear LAB color space, but write the result in sRGB. Do not 9 | # write gamma information in the png metadata though. This feature of png 10 | # causes more trouble than it solves, because it is often handled incorrectly. 11 | # Programs and graphics APIs make wrong assumptions about the color space or 12 | # try to apply corrections at the wrong point. Just strip gamma information and 13 | # hope the entire pipeline is sRGB. 14 | 15 | # Sometimes resizing with `-resize` instead of `-distort Resize` can produce a 16 | # smaller output image. 17 | 18 | convert $1 \ 19 | -colorspace LAB \ 20 | -filter Lanczos2 \ 21 | -distort Resize 1280 \ 22 | -colorspace sRGB \ 23 | -define png:exclude-chunk=gAMA \ 24 | -define png:exclude-chunk=cHRM \ 25 | $2 26 | -------------------------------------------------------------------------------- /images/resized/geomancer-move-mage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruuda/blog/bab1f438a0057239b9e0ec41007a1ed8b8dbc4b6/images/resized/geomancer-move-mage.png -------------------------------------------------------------------------------- /images/resized/geomancer-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruuda/blog/bab1f438a0057239b9e0ec41007a1ed8b8dbc4b6/images/resized/geomancer-overview.png -------------------------------------------------------------------------------- /images/resized/geomancer-soldier-under-attack-by-mage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruuda/blog/bab1f438a0057239b9e0ec41007a1ed8b8dbc4b6/images/resized/geomancer-soldier-under-attack-by-mage.png -------------------------------------------------------------------------------- /images/resized/richys-groceries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruuda/blog/bab1f438a0057239b9e0ec41007a1ed8b8dbc4b6/images/resized/richys-groceries.png -------------------------------------------------------------------------------- /images/resized/robigo-luculenta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruuda/blog/bab1f438a0057239b9e0ec41007a1ed8b8dbc4b6/images/resized/robigo-luculenta.png -------------------------------------------------------------------------------- /images/resized/the-small-bang-theory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruuda/blog/bab1f438a0057239b9e0ec41007a1ed8b8dbc4b6/images/resized/the-small-bang-theory.png -------------------------------------------------------------------------------- /images/resized/tristar-cyan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruuda/blog/bab1f438a0057239b9e0ec41007a1ed8b8dbc4b6/images/resized/tristar-cyan.png -------------------------------------------------------------------------------- /posts/a-language-for-designing-slides.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: A language for designing slides 3 | break: language for 4 | date: 2017-04-27 5 | minutes: 6 6 | lang: en-GB 7 | synopsis: No existing tool allowed me to design slides in the way I would like. So I built my own. 8 | run-in: It is my opinion that 9 | teaser: a-reasonable-configuration-language 10 | --- 11 | 12 | It is my opinion that great slides are designed. 13 | Applying a fancy template to a dull set of bullet points has never been easier than today, 14 | and good slides need not be beautiful. 15 | But great slides -- great slides are *designed*. 16 | As a programmer with a secret love for typography, 17 | and a slight control freak, 18 | none of the tools that I have used allow me to design slides in the way I would like. 19 | So I built my own. 20 | 21 | The status quo 22 | -------------- 23 | 24 | Design requires absolute control of where graphics are placed. 25 | This level of control has traditionally been reserved for graphical editors 26 | such as Powerpoint and Illustrator. 27 | Although these editors allow full control, 28 | they come with a few serious drawbacks. 29 | Their binary formats are not friendly to source control, for instance. 30 | But the real problem is that **operations in a visual editor do not compose**. 31 | Anyone who has ever tried to animate a diagram in Powerpoint 32 | should understand what I mean here. 33 | The way it is done, is rougly like this: 34 | 35 | 1. Draw the most complete version of the diagram. 36 | 2. Copy it onto a few slides. 37 | 3. Edit every copy to create the individual frames. 38 | 39 | And if after that you want to change the colour of a piece of text 40 | that occurs on all slides, 41 | or move it a bit ... 42 | then well, you are stuck. 43 | If the change is minor, 44 | you might try to do exactly the same edit on all slides. 45 | If the slides do not differ much from the base diagram, 46 | deleting them all and starting over with fresh copies might be the best option. 47 | Obviously neither of these are desirable. 48 | 49 | The solution then, 50 | is to not edit the diagram directly, 51 | but to generate it from some kind of specification. 52 | TikZ in a Beamer presentation is a good example of this, 53 | and it solves the diagram problem well. 54 | I have used it with success for presentations about Git, 55 | with graph drawings to explain how operations manipulate the DAG. 56 | The Fontspec package gives me full typographic control, 57 | and TeX files are friendly to source control. 58 | Getting to an initial version of a drawing is more work 59 | due to the longer feedback loop, 60 | but that is the tradeoff for being able to make edits later on. 61 | But although Beamer satisfies many of my needs, 62 | it is fundametally not the tool that I wish for. 63 | 64 | Beamer is a LaTeX package, 65 | and as such it does not offer precise control over lay-out. 66 | The entire point of TeX is that it takes care of lay-out for you. 67 | For long documents full of text this makes sense. 68 | But for slides with little text, I want control. 69 | One could do everything in an embedded TikZ drawing, 70 | but this is tedious. 71 | TikZ and similar systems such as Metapost are great for complex drawings, 72 | but not ergonomic for typographic design. 73 | Although the basic drawing primitives compose, 74 | **parametrising and reusing graphics is difficult**. 75 | Macros are awkward and in many ways limited. 76 | 77 | Different fundamentals 78 | ---------------------- 79 | 80 | The problem with TikZ and similar systems is twofold. 81 | Firstly, they are too domain-specific to make automating things viable. 82 | Macro definitions are no substitute for variables or functions, 83 | because they deal with tokens, not values. 84 | It is like C without functions, only preprocessor macros. 85 | Secondly, all of the drawing DSLs that I have used manipulate a canvas directly. 86 | This means that the only available mechanism for reuse is necessarily procedural. 87 | Draw calls might be grouped in a procedure and parametrised over some inputs, 88 | but this approach is fundamentally limited. 89 | 90 | Let me demonstrate this limitation with an example. 91 | Say I have a procedure that draws a rectangle 92 | with its top-left corner at a given coordinate. 93 | How do I draw it with its centre at a given coordinate? 94 | I would have to know its size beforehand, 95 | and offset the input coordinates appropriately. 96 | For a rectangle this is doable, 97 | but for more complex shapes, 98 | computing the size beforehand quickly becomes impractical. 99 | 100 | ![Two rectangles, aligned top-left, and centre.](/images/rectangles.svg) 101 | 102 | The issue with the procedural approach 103 | is that once graphics are drawn, 104 | they are set in stone. 105 | What I would like instead, 106 | is a system where graphics are first class. 107 | Where they can be inspected and manipulated *after* being drawn. 108 | And I want full scripting with proper functions. 109 | 110 | I started writing down things in a hypothetical language, 111 | to get a clear picture of what my ideal tool would look like. 112 | For a while I investigated building an embedded DSL 113 | in a general-purpose scripting language like Python or Lua, 114 | but it quickly became clear to me 115 | that these were going to be too noisy to be practical. 116 | And so I started working on an interpreter for that hypothetical language. 117 | Today it is no longer hypothetical. 118 | I called the domain specific language *Pris*. 119 | The interpreter is written in Rust, 120 | and it uses Cairo and Harfbuzz for rendering and font shaping. 121 | 122 | Pris 123 | ---- 124 | 125 | So how does Pris solve the rectangle problem? 126 | Drawing a rectangle with its top-left corner at a given location 127 | is not so different from other tools: 128 | 129 | { 130 | at (1em, 1em) put fill_rectangle((1em, 1em)) 131 | } 132 | 133 | The challenge now is to put the rectangle 134 | with its centre at `(1em, 1em)`, 135 | without using the knowledge that its sides are `1em` long. 136 | In Pris, that is done as follows: 137 | 138 | { 139 | rect = fill_rectangle((1em, 1em)) 140 | at (1em, 1em) - rect.size * 0.5 put rect 141 | } 142 | 143 | This example shows that graphics are *first class*: 144 | they can be assigned to variables, 145 | and their size can be queried. 146 | Nothing is drawn until something is `put` on the canvas. 147 | Coordinate arithmetic is also supported out of the box. 148 | 149 | To take this one step further, 150 | we might extract the alignment logic into a function: 151 | 152 | center = function(frame) 153 | { 154 | at frame.size * -0.5 put frame 155 | } 156 | 157 | { 158 | at (1em, 1em) put center(fill_rectangle((1em, 1em))) 159 | } 160 | 161 | This time there is a `put` inside the function, 162 | but it does not draw anything on the main canvas like a procedure would do. 163 | Functions in Pris are pure, free of side effects. 164 | The `put` only places a graphic locally in the scope of the function. 165 | The result of calling the function is itself a new graphic, 166 | which can be placed on the main canvas (demarked by bare braces). 167 | This example also shows that functions are first class values. 168 | 169 | Small caveat: the `center` function will do the wrong thing 170 | if the bounding box extends to the left of, or above `(0,` `0)`. 171 | The variables for doing proper alignment in that case are not yet exposed. 172 | 173 | Progress report 174 | --------------- 175 | 176 | An experimental implementation of Pris exists. 177 | It is free software, 178 | [available on GitHub](https://github.com/ruuda/pris#readme). 179 | The current feature set is limited, 180 | and I intend to make changes to the syntax and exposed functions still. 181 | Nonetheless, I have used it to do one set of slides so far. 182 | This has helped me prioritise features, 183 | and to sort out what works and what doesn’t. 184 | The number of implemented primitives is small, 185 | but placing SVG graphics and rendering text is supported, 186 | which for many things is sufficient. 187 | I plan to continue in the same way: 188 | implement features as I need them, 189 | and make things more ergonomic when they start to become unwieldy. 190 | 191 | If Pris seems useful to you, 192 | then please go ahead and try it. 193 | I will have to invest more in diagnostics and documentation; 194 | for now there are only [the examples](https://github.com/ruuda/pris/tree/master/examples). 195 | There are no binaries available yet, 196 | but the build process is straightforward. 197 | If you get stuck somewhere then feel free to [reach out](/contact) to me. 198 | And if you have any insights, please do share them. 199 | -------------------------------------------------------------------------------- /posts/a-perspective-shift-on-amms-through-mev.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: A perspective shift on automated market makers through MEV 3 | date: 2021-12-07 4 | minutes: 4 5 | lang: en-US 6 | synopsis: A swap at an automated market maker is not like a market order, it behaves more like a limit order. This is the change of perspective that MEV is forcing us to make. 7 | run-in: It takes time 8 | --- 9 | 10 | It takes time for new ideas to be properly understood. 11 | Often the way we first formulate an idea, 12 | is in hindsight not the simplest or the most elegant way, 13 | and a change of perspective can lead to new understanding. 14 | I think we are in this situation with automated market makers right now, 15 | and MEV ([miner/maximum extractable value][mev]) is forcing a perspective shift. 16 | 17 | Automated market makers (AMMs for short) are a relatively recent development, 18 | and although the math behind swaps is well understood, 19 | I don’t think that this is the full picture. 20 | Even if the swap itself is well understood, the mechanism can be flawed. 21 | 22 | [Flash liquidity in Uniswap v3][flashlp] is a clear example of this: 23 | it’s a mechanism that admits a profitable strategy which was not intended by the developers, 24 | and this profit comes at the expense of the intended users (regular liquidity providers). 25 | In a sense, it’s a game-theoretic vulnerability in the protocol. 26 | Sometimes these flaws can be fixed at the protocol level, 27 | but sometimes they indicate a flaw in our understanding. 28 | 29 | [mev]: https://ethereum.org/en/developers/docs/mev/ 30 | [flashlp]: https://twitter.com/revertfinance/status/1409642606082940930 31 | 32 | Swaps are limit orders, not market orders 33 | ----------------------------------------- 34 | 35 | My original understanding of AMM swaps was that they are like market orders: 36 | you trade asset X for asset Y, 37 | and you pay whatever the pool’s current price is at the time the swap executes. 38 | When the swap executes, the pool price may be different 39 | from the price at the time when you entered the order, 40 | and to protect against unexpected expenses, 41 | you can enter a maximum slippage percentage. 42 | This sets the maximum price that you are willing to pay for the asset. 43 | If the pool price is above your maximum when the swap executes, 44 | the transaction fails. 45 | I think the intent of the max slippage was just that: 46 | to limit slippage at busy times, 47 | when the price can change due to other swaps executing before yours. 48 | 49 | All was well for a while, 50 | until miners discovered _miner extractable value_ 51 | (now also called _maximal extractable value_), or MEV for short. 52 | Extractors started [sandwiching][sandwiching] swaps, 53 | and the effect of that is that you always pay your maximum price, 54 | not the pool price. 55 | 56 | In a sense, 57 | in the presence of sandwiching, 58 | a swap behaves more like a limit order than a market order: 59 | if it executes at all, it executes at the price that _you_ set, 60 | not at the market price. 61 | Like with a limit order, 62 | the price you pick is a trade-off: 63 | at lower prices your order might not be filled (your transaction may fail), 64 | at higher prices you risk overpaying. 65 | 66 | [sandwiching]: https://ethereum.org/en/developers/docs/mev/#mev-examples-sandwich-trading 67 | 68 | If you accept sandwiching as a fact of life, 69 | then our understanding of AMMs is wrong: 70 | the pool price is not the market price, 71 | it’s a minimum price. 72 | We’ve got our user interfaces backwards: 73 | we shouldn’t display the pool price 74 | and then hide the max slippage under advanced settings, 75 | we should own up to it and include the selected slippage in the price. 76 | 77 | Make it a feature, not a bug 78 | ---------------------------- 79 | 80 | If you look at it from this point of view, 81 | isn’t it crazy that we let MEV extractors 82 | take the difference between the pool price and the maximum price? 83 | Isn’t that a flaw in the AMM’s design? 84 | A game-theoretic vulnerability? 85 | 86 | So here is a proposal: **let the AMM take the difference instead**. 87 | Users who swap always pay their maximum price, 88 | and the difference between the pool price and the maximum price 89 | goes to liquidity providers, as part of the swap fee. 90 | For users, nothing changes. 91 | They pay their maximum price either way. 92 | Liquidity providers benefit, 93 | and the MEV opportunity goes away. 94 | 95 | Conclusion 96 | ---------- 97 | 98 | When software contains a critical vulnerability, 99 | we frown upon the attackers who exploit it, 100 | but we also recognize that the only way to address the issue is by patching the software. 101 | What is really bad, 102 | is a software vendor who leaves a vulnerability unpatched 103 | when it is being exploited in the wild. 104 | 105 | I think we should approach sandwiching 106 | — and unintended MEV opportunities more 107 | generally — the same way: as a flaw that needs to be patched. 108 | I frown upon the MEV extractors who sandwich. 109 | But at the same time, I think the real blame is with AMMs 110 | for not doing anything about it. 111 | This is not obvious if you think of AMM swaps as market orders. 112 | But MEV is forcing us to change our view, 113 | and recognize that AMM swaps really do have a limit price. 114 | -------------------------------------------------------------------------------- /posts/an-api-for-my-christmas-tree.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: An API for my Christmas tree 3 | break: my Christmas 4 | date: 2017-12-01 5 | minutes: 3 6 | lang: en-GB 7 | synopsis: With a tiny server, and an Arduino to drive the lights, I connected my Christmas tree to the internet. 8 | run-in: It is almost 9 | --- 10 | 11 | It is almost that time of the year again. 12 | Time to bring out the Christmas tree, 13 | the Arduino, and the RGB LED strands. 14 | Let’s have a bit of fun connecting my tree to the internet. 15 | 16 | Hardware 17 | -------- 18 | 19 | The [LEDs I had lying around][leds] 20 | are the perfect lights for a Christmas tree 21 | -- the brightly coloured wires just give it that nice touch of do-it-yourself electronics. 22 | The LEDs can be addressed individually using an Arduino Uno. 23 | Adafruit provides an [Arduino program][adalight] 24 | that accepts colour data over a serial connection, 25 | and drives the LED strand. 26 | The only thing left is to feed it interesting colours. 27 | 28 | [leds]: https://www.adafruit.com/product/322 29 | [adalight]: https://github.com/adafruit/Adalight 30 | 31 | Software 32 | -------- 33 | 34 | I wrote a small program that computes the colour of every LED 35 | as a function of LED number and time. 36 | Different functions can produce effects such as blinking, 37 | or an animated rainbow. 38 | The program runs on a local machine, 39 | to which the Arduino is connected via USB. 40 | I did not want to run an internet-exposed server on this machine, 41 | so the program acts as a client, 42 | and receives updates from a server about the colour function to use. 43 | 44 | The server program exposes a simple REST API 45 | with endpoints to change the color function, 46 | or to temporarily blink in a given color. 47 | On a different port the server accepts TCP connections from the client progam. 48 | Over this connection, the server broadcasts changes to the color function. 49 | The server supports basic access control: 50 | the API is protected by a password, 51 | and only available over https. 52 | The connection between the client program and server is unencrypted though, 53 | and there is no authentication there. 54 | Beware of men in the middle messing with your Christmas lights. 55 | 56 | The client and server program are free software, 57 | [available on GitHub][ct-gh]. 58 | Both are written in Haskell, 59 | so a simple `stack build` will produce two binaries 60 | with no runtime dependencies apart from a few system libraries. 61 | I copied over the server binary to a cheap cloud instance, 62 | set up a Letsencrypt certificate, 63 | and I was good to go. 64 | An example systemd unit is included in the repository. 65 | 66 | [ct-gh]: https://github.com/ruuda/christmas-tree 67 | 68 | Deployment 69 | ---------- 70 | 71 | At this point I could send an API call to the server, 72 | and the pattern in the tree would change. 73 | That was pretty cool -- 74 | somehow making lights blink always feels magical. 75 | But what do you do with an internet-controlled Christmas tree? 76 | You hook it up to the build system, of course! 77 | Last year I deployed the lights at work and put this in our `.travis.yml`: 78 | 79 | ```yml 80 | after_success: 81 | # Make the Christmas tree blink green. 82 | - curl -X POST 'https://chainsaw:pass@tree.example.com/blink?color=00ff00&seconds=10' 83 | 84 | after_failure: 85 | # Make the Christmas tree blink red. 86 | - curl -X POST 'https://chainsaw:pass@tree.example.com/blink?color=ff0000&seconds=10' 87 | ``` 88 | 89 | This even turned out to be semi-useful for a short while 90 | -- until my colleagues found out 91 | that they could also make the tree blink red 92 | from their local workstations ... 93 | Anyways, happy holidays! 94 | -------------------------------------------------------------------------------- /posts/building-elm-with-stack.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Building Elm with Stack 3 | break: Elm with 4 | date: 2017-09-21 5 | minutes: 4 6 | lang: en-GB 7 | synopsis: The Haskell tool Stack enables simple reproducible builds. I used it to build the Elm compiler from source. 8 | run-in: The hackathon I joined 9 | --- 10 | 11 | The hackathon I joined last weekend was a nice opportunity to try the [Elm][elm] language. 12 | Getting the Elm compiler on my system proved harder than expected though, 13 | and I ended up building it from source. 14 | It is a nice excuse for me to share a few thoughts on package management and reproducible builds. 15 | If you just want to build Elm 0.18 from source, 16 | you can skip straight to the [`stack.yaml`](#building-elm-with-stack). 17 | 18 | Prelude 19 | ------- 20 | My first attempt to get the Elm platform onto my Arch Linux system was not succesful. 21 | I had tried Elm before about a year ago, 22 | and I still had an installation of an older version of the Elm platform on my machine, 23 | courtesey of the [`elm-platform`][elm-platform] package from the Arch User Repository. 24 | The Elm compiler is written in Haskell, 25 | and the `elm-platform` package builds it from source. 26 | A new version of the package was available, 27 | but it failed to build, 28 | because Arch currently ships a newer version of GHC than the one Elm requires to build. 29 | And frankly I did not want to install the GHC and Cabal packages to build it either. 30 | A few months ago their Arch maintainer started packaging all Haskell dependencies as individual packages, 31 | which causes a large amount of churn during updates, 32 | and requires significant additional disk space. 33 | I uninstalled them because I use [Stack][stack] for Haskell development, 34 | which can manage GHC anyway. 35 | It has a fine [`stack-static`][stack-static] package in the AUR. 36 | 37 | With no working package in the official Arch repositories or the AUR, 38 | I turned to the Elm website to check out the recommended installation method. 39 | It told me to `npm install` or build from source. 40 | My system already has a package manager that I use to keep programs up to date, 41 | and I refuse to install yet another package manager that I would have to run to perform management tasks. 42 | (Especially if that involves running Javascript outside of a browser.) 43 | The role of a language package manager should be to manage build-time dependencies, 44 | not to distribute user-facing programs to end users. 45 | I decided to build from source. 46 | 47 | Elm provides [`BuildFromSource.hs`][fromsource], a script that automates building from source. 48 | It demands GHC 7.10, 49 | so I checked [stackage.org][stackage] to find a Stackage snaphot based on that compiler, 50 | and I ran the script with with the right version of `runghc`: 51 | 52 | stack setup --resolver lts-6.35 53 | stack runghc --resolver lts-6.35 BuildFromSource.hs 0.18 54 | 55 | After commenting out a check that verifies that Cabal is available, 56 | it cloned the Elm repositories. 57 | Then it failed, 58 | because indeed I did not have a `cabal` binary. 59 | Reluctantly I installed Arch’s Cabal package after all, 60 | but this got me no further; 61 | building now failed with a complaint about GHC package paths. 62 | Rather than digging deeper, 63 | I figured it would be easier to make the projects build with Stack. 64 | 65 | Building Elm with Stack 66 | ----------------------- 67 | 68 | I added the following `stack.yaml` to the directory that contained the various Elm repositories: 69 | 70 | ```yaml 71 | resolver: lts-6.35 72 | allow-newer: true 73 | packages: 74 | - elm-compiler 75 | - elm-make 76 | - elm-package 77 | - elm-reactor 78 | - elm-repl 79 | ``` 80 | 81 | **This was all that was required, really.** 82 | After a single `stack build`, 83 | the binaries were in `$(stack path --local-install-root)/bin`. 84 | It is a bit embarassing how easy this was, 85 | after all the things I had tried before. 86 | 87 | If you want to try this yourself, 88 | there is no need to use Elm’s `BuildFromSource.hs` to clone the repositories. 89 | You can just clone them directly: 90 | 91 | ```sh 92 | for project in compiler make package reactor repl; do 93 | git clone https://github.com/elm-lang/elm-$project --branch 0.18.0 94 | done 95 | ``` 96 | 97 | Reproducible builds 98 | ------------------- 99 | This adventure with Elm is in my opinion a good example of a bigger issue with reproducible builds, 100 | or builds that are *producible* at all. 101 | If building a given commit produces a binary on one machine, 102 | I want it to produce the same binary on a different machine. 103 | Ideally it should be bitwise identical, 104 | although that is often difficult for unfortunate reasons. 105 | But at the very least the source fed into the compiler should be identical. 106 | And if a given commit compiles today, 107 | I want it to still compile three years from now. 108 | Making a build reproducible in this sense 109 | requires pinning the versions of all dependencies. 110 | And if future compiler releases are not backwards compatible, 111 | the compiler must be pinned as well. 112 | Recording that metadata is one thing, 113 | but obtaining the right compiler can be difficult in practice. 114 | Some languages solve this with another layer of indirection, 115 | by having a version manager manage the package manager and compiler or runtime. 116 | But I think Stack got it right here by making the build tool manage the compiler: 117 | `stack build` just works, 118 | and it does the right thing. 119 | 120 | Somewhat ironically, 121 | if I want to compile multiple projects that use different Elm versions, 122 | I will need to be able to switch Elm binaries. 123 | Fortunately I now have a way to obtain the binaries for a given release that is not too painful, 124 | and I can live with having to modify the `PATH` occasionally. 125 | In many ways this is even preferable over having a single system-wide version, 126 | because multiple versions can coexist. 127 | 128 | In the end I got Elm working with just enough time left for the hackathon. 129 | More about that soon. 130 | 131 | [elm]: http://elm-lang.org/ 132 | [elm-platform]: https://aur.archlinux.org/packages/elm-platform/ 133 | [stack]: https://haskellstack.org 134 | [stack-static]: https://aur.archlinux.org/packages/stack-static/ 135 | [fromsource]: https://github.com/elm-lang/elm-platform/blob/c83832cd38091033288a62d8b8ce9f1694454d9a/installers/BuildFromSource.hs 136 | [stackage]: https://www.stackage.org/ 137 | -------------------------------------------------------------------------------- /posts/continuations-revisited.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Continuations revisited 3 | date: 2013-08-12 4 | lang: en-GB 5 | synopsis: Observables are a different take on asynchronous programming that can be more powerful than tasks for some scenarios. 6 | run-in: A while ago 7 | --- 8 | 9 | A while ago I wrote about how `Task` [is a monad](/2013/05/01/the-task-monad-in-csharp), 10 | and about how that simplifies composition. Today, I want to look at observables, 11 | and how they can be used as a replacement for tasks. 12 | 13 | Observables, which are part of the Reactive Framework (Rx), are not yet 14 | included in the .NET base class library. The easiest way to get them is 15 | through [NuGet](https://www.nuget.org/packages/Rx-Main). A lot has been 16 | written about Rx already, and there are some great videos on Channel 9 17 | as well. `IObservable` [is the dual](http://channel9.msdn.com/shows/Going+Deep/E2E-Erik-Meijer-and-Wes-Dyer-Reactive-Framework-Rx-Under-the-Hood-1-of-2/) 18 | of `IEnumerable`. If `IEnumerable` is a ‘pull’-based model, then 19 | `IObservable` is ‘push’. It can push values to an observer, as well as 20 | an ‘end-of-sequence’ signal or an error. 21 | 22 | Tasks vs. observables 23 | --------------------- 24 | Let’s compare this to `Task`. A `Task` represents a value of `T` that is 25 | either available, or will be available in the future. Additionally, the task 26 | catches exceptions, so instead of resulting in a value in the future, it 27 | might result in an exception instead. This maps directly to observables: 28 | an observable represents values that will be pushed in the future, but 29 | it might push an exception instead. The difference is that an `IObservable` 30 | might push multiple values of `T`, whereas a `Task` will result in at 31 | most one value of `T`. 32 | 33 | A task can be continued with a delegate: the delegate will be called when 34 | the task is completed. This is exactly a push-based model! You register 35 | a continuation in advance, and the task will call it when the value is 36 | available. With observables, you subscribe an observer, on which the 37 | `OnNext` method will be called when the value is ready. There is a 38 | striking similarity between tasks and observables here, and it seems 39 | that ‘one-shot observables’ (which push only one value) can replace 40 | tasks. This is indeed the case, and Rx even provides extension methods 41 | to convert between the two. 42 | 43 | Let’s consider the scenario of the previous post again, but now using observables. 44 | We have classes `A`, `B`, `C`, and `D`, and the following methods: 45 | 46 | ```cs 47 | IObservable Method1(A a) { ... } 48 | IObservable Method2(B b) { ... } 49 | IObservable Method3(C c) { ... } 50 | ``` 51 | 52 | I am looking for an elegant composition operation that allows me to chain 53 | these methods together. `Task` turned out to be a monad, and the 54 | monadic composition operation (bind), was exactly the operation that I 55 | was looking for. `IObservable` is a monad as well, so maybe its 56 | implementation of bind is what I am looking for here? The method with 57 | the right signature is called `SelectMany`. Given an `IObservable` 58 | and a `Func>`, it creates an `IObservable`. It 59 | turns out, that this is indeed the correct composition operation, and 60 | that it represents continuations! Composition is as easy as: 61 | 62 | ```cs 63 | IObservable Composed(A a) 64 | { 65 | return Method1(a).SelectMany(Method2).SelectMany(Method3); 66 | } 67 | ``` 68 | 69 | The method name is not very descriptive here, and using `SelectMany` to 70 | do `Then` seems rather odd. `SelectMany` is a LINQ operator inherited 71 | from enumerables, so we are stuck with it. Note that because Rx is so 72 | widely applicable it _cannot_ have one name that describes every 73 | behaviour of bind. (Therefore it is usually called ‘bind’, which has 74 | little intrinsic meaning outside of monads. Haskell even avoids all 75 | intrinsic meaning by using a symbol.) In practice, adding aliases for 76 | existing methods ultimately leads to more confusion, so I would **not** 77 | recommend to make an extension method `Then` which is a `SelectMany` in 78 | disguise. (For a real life example of methods that make things “easier”: 79 | when should you use `Stream.Close`, and when `Stream.Dispose`? Hint: the 80 | documentation for framework [4.0](http://msdn.microsoft.com/en-us/library/system.io.stream.close%28v=vs.100%29.aspx) 81 | differs from [4.5](http://msdn.microsoft.com/en-us/library/system.io.stream.close%28v=vs.110%29.aspx).) 82 | 83 | On a side note: as opposed to `Task`, `IObservable` _does_ 84 | implement the full set of monadic operations by default. The 85 | implementation of ‘return’ -- which takes a `T` and returns an 86 | `IObservable` -- is simply called `Return`. 87 | 88 | More composition 89 | ---------------- 90 | Let’s consider a different scenario, and solve it with tasks as well as observables. 91 | Given a list of objects (say, of type `A`), first appy method `M1` to them in 92 | parallel. When all of the calls are done, apply method `M2` in parallel, 93 | and then block until all those calls are done. 94 | 95 | Even though the Task Parallel Library has `ContinueWhenAll`, I still find this 96 | clumsy without C# 5 `await` support: 97 | 98 | ```cs 99 | IEnumerable objects = ... 100 | 101 | var tasks1 = objects.Select(a => Task.Factory.StartNew(() => a.M1())); 102 | var task = Task.Factory.ContinueWhenAll(tasks1.ToArray(), _ => 103 | { 104 | var tasks2 = objects.Select(a => Task.Factory.StartNew(() => a.M2())); 105 | Task.WaitAll(tasks2.ToArray()); 106 | }); 107 | 108 | // Do something while the tasks are running, then wait. 109 | task.Wait(); 110 | ``` 111 | 112 | Again, tasks lack a composition operation, the current solution does not scale 113 | well. What if I wanted to call `M1` ... `M7` sequentially (but the methods applied 114 | to the instances in parallel)? Furthermore, the continuation blocks, which wastes 115 | a thread. Let’s try that again, using the `Then` method from the [previous post](/2013/05/01/the-task-monad-in-csharp): 116 | 117 | ```cs 118 | Func, Action, Task> doParallel = (xs, action) => 119 | { 120 | var tcs = new TaskCompletionSource(); 121 | var tasks = xs.Select(a => Task.Factory.StartNew(() => action(a))); 122 | Task.Factory.ContinueWhenAll(tasks.ToArray(), _ => tcs.SetResult(null)); 123 | return tcs.Task; 124 | }; 125 | 126 | IEnumerable objects = ... 127 | 128 | var task = doParallel(objects, a => a.M1()) 129 | .Then(() => doParallel(objects, a => a.M2())); 130 | 131 | // Do something while the tasks are running, then wait. 132 | task.Wait(); 133 | ``` 134 | 135 | This does scale to more continuations, and it does not waste a thread. It still 136 | has a problem though: exceptions are not handled properly. It would require a lot 137 | of boilerplate code to solve this quite trivial problem. .NET 4.5 improves 138 | the situation a bit, there you can use: 139 | 140 | ```cs 141 | Func, Action, Task> doParallel = (xs, action) => 142 | { 143 | var tasks = xs.Select(a => Task.Factory.StartNew(() => action(a))); 144 | return Task.WhenAll(tasks.ToArray()); 145 | }; 146 | ``` 147 | 148 | This will handle exceptions correctly as well. There might be a better 149 | solution that I am unaware of though, please let me know if you have one! 150 | 151 | How about observables? I find this version more attractive than its TPL 152 | equivalent: 153 | 154 | ```cs 155 | IEnumerable objects = ... 156 | 157 | var observables = Observable.Concat 158 | ( 159 | objects.ToObservable().SelectMany(a => Observable.Start(() => a.M1())), 160 | objects.ToObservable().SelectMany(a => Observable.Start(() => a.M2())) 161 | ) 162 | .Publish(); // Do not wait until subscription. 163 | 164 | // Do something while the observables are running, then wait. 165 | observables.Wait(); 166 | ``` 167 | 168 | It scales well to many continuations, it is simple, and it handles exceptions 169 | properly. One thing I find remarkable, is that Rx was not specifically designed 170 | for this scenario: it provides elegant solutions to many problem domains! 171 | 172 | Note that in this case, PLINQ is even easier, if you do not need the 173 | calling thread to do anything in parallel with the calls: 174 | 175 | ```cs 176 | IEnumerable objects = ... 177 | 178 | objects.AsParallel().ForAll(a => a.M1()); 179 | objects.AsParallel().ForAll(a => a.M2()); 180 | ``` 181 | 182 | However, PLINQ only provides a solution to very basic problems. What if there 183 | was an `M1Async`, that returns either a `Task` or `IObservable`? Converting 184 | it to a synchronous method by appending `.Wait()` works, but in the case of 185 | PLINQ this wastes a thread per concurrent call. The TPL and Rx ways above 186 | actually become simpler for `M1Async`, because asynchronicity need not be 187 | introduced explicitly any more. 188 | 189 | TL;DR: although the Task Parallel Library greatly simplified asynchronous 190 | programming in C#, observables can do anything tasks can do, with the additional 191 | benefit of better composability. 192 | -------------------------------------------------------------------------------- /posts/csharp-await-is-the-haskell-do-notation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: C# await is the Haskell do notation 3 | break: the Haskell 4 | date: 2013-08-20 5 | lang: en-GB 6 | synopsis: When writing asynchronous code in C# with the await keyword, the emerging structure resembles the Haskell do notation. 7 | run-in: As a follow-up 8 | --- 9 | 10 | As a follow-up to the [task monad](/2013/05/01/the-task-monad-in-csharp), 11 | let’s make a comparison between the new `async` and `await` syntax in C# 5, 12 | and the do notation in Haskell. Two constructs that might seem unrelated at 13 | first, allow code to be written in a form that is _exactly_ the same. 14 | 15 | The do notation 16 | --------------- 17 | Haskellers figured out that monads could be used to do IO a long time ago, but 18 | it was clumsy. Consider the classic example of asking a user for a name and 19 | printing it back. In Haskell, we would start with: 20 | 21 | ```haskell 22 | askFirstName = putStrLn "What is your first name?" >> getLine 23 | askLastName = putStrLn "What is your last name?" >> getLine 24 | sayHi firstName lastName = putStrLn $ "Hello " ++ firstName ++ 25 | " " ++ lastName ++ "." 26 | ``` 27 | 28 | For those new to Haskell, the `>>` operator here sequences two operations, so 29 | that the message is printed before a name is read. The `$` is like an opening 30 | parenthesis with an implicit closing parenthesis, and `++` concatenates strings. 31 | 32 | In normal, synchronous C#, this could be written as follows: 33 | 34 | ```cs 35 | string AskFirstName() 36 | { 37 | Console.WriteLine("What is your first name?"); 38 | return Console.ReadLine(); 39 | } 40 | 41 | string AskLastName() 42 | { 43 | Console.WriteLine("What is your last name?"); 44 | return Console.ReadLine(); 45 | } 46 | 47 | void SayHi(string firstName, string lastName) 48 | { 49 | Console.WriteLine("Hello {0} {1}.", firstName, lastName); 50 | } 51 | ``` 52 | 53 | Now these functions and methods need to be composed to create the program. 54 | Without the do notation, the Haskell version is acceptable, but misleading. 55 | The indentation that I use here is allowed, but it hides the fact that 56 | lambdas are nested deeper and deeper. 57 | 58 | ```haskell 59 | main = askFirstName 60 | >>= \firstName -> askLastName 61 | >>= \lastName -> sayHi firstName lastName 62 | ``` 63 | 64 | The `>>=` is the monadic ‘bind’ operator, which in this case ensures that its 65 | left hand side (the previous line) is executed before its right hand side. In 66 | Haskell, the `\` introduces a lambda function, with arguments before the `->`, 67 | and the body after it. The C# analog looks more comprehensible: 68 | 69 | ```cs 70 | void Main() 71 | { 72 | string firstName = AskFirstName(); 73 | string lastName = AskLastName(); 74 | SayHi(firstName, lastName); 75 | } 76 | ``` 77 | 78 | Expressing this kind of sequential code imperatively just makes more sense, 79 | so the Haskell people came up with the _do notation_. The do notation allows 80 | one to write the following: 81 | 82 | ```haskell 83 | main = do 84 | firstName <- askFirstName 85 | lastName <- askLastName 86 | sayHi firstName lastName 87 | ``` 88 | 89 | As you can see, this resembles an imperative programming style. The compiler 90 | translates it into the first version. With monads and the do notation, Haskell 91 | has powerful tools to write [pure](https://en.wikipedia.org/wiki/Pure_function) 92 | progams in a convenient manner. 93 | 94 | Re-discovering the wheel 95 | ------------------------ 96 | So far, the C# version has been synchronous. What if `Console` offered 97 | asynchronous methods? This might seem far-fetched, but if you replace our simple 98 | example with a network socket in a high-performance web service, it makes 99 | a lot of sense to use asynchronous tasks if the work is IO-bound. Let’s 100 | pretend that we have `AskFirstNameAsync` which asks for a name and reads it from 101 | the console in a non-blocking way. It should return a `Task`. Similarly, 102 | we need `AskLastNameAsync`, and `SayHiAsync` which returns a `Task`. If we use 103 | some of the `Then` [methods](https://blogs.msdn.com/b/pfxteam/archive/2010/11/21/10094564.aspx) 104 | that I have talked about before, the async program could be written as follows: 105 | 106 | ```cs 107 | Task MainAsync() 108 | { 109 | return AskFirstNameAsync() 110 | .Then(firstName => AskLastNameAsync() 111 | .Then(lastName => SayHiAsync(firstName, lastName))); 112 | } 113 | ``` 114 | 115 | Except for syntactic differences, this version has _exactly_ the same form as 116 | the first Haskell attempt. (Note that the indentation is misleading here as well.) 117 | Just like the Haskellers, the C# people recognised that this is a not the best 118 | way to write non-blocking code, so they introduced `await`. With `await`, the 119 | code can be re-written as follows: 120 | 121 | ```cs 122 | async Task MainAsync() 123 | { 124 | string firstName = await AskFirstNameAsync(); 125 | string lastName = await AskLastNameAsync(); 126 | await SayHiAsync(firstName, lastName); 127 | } 128 | ``` 129 | 130 | Better indeed, but the remarkable thing is -- this code has _exactly_ the same 131 | structure as the Haskell version with do notation! I do not know whether the C# 132 | designers were inspired by the do notation, but it is striking that the 133 | constructs are so similar, both in the clumsy syntax, as well as the improved 134 | syntax. Also note the similarity between asynchronous C# and Haskell: Haskell 135 | code naturally has this asynchronous form, and it supports non-blocking IO with 136 | minimal effort. 137 | -------------------------------------------------------------------------------- /posts/encryption-by-default.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Encryption by default 3 | date: 2015-01-13 4 | lang: en-GB 5 | synopsis: You should encrypt all of your internet traffic. I am helping by making my site available over https. 6 | run-in: In recent years 7 | --- 8 | 9 | In recent years it has been revealed that tapping of internet traffic is happening at very large scales. 10 | Automated eavesdropping of an astounding portion of all internet traffic is a fact of life. 11 | Legislation will not stop agencies and other parties from hoarding your data. 12 | It might make it illegal, but not impossible. 13 | The only way to really stop agencies from listening in on you, is to make it impossible. 14 | Use encryption, and encrypt _all_ of your internet traffic. 15 | Encryption should be the default. 16 | 17 | As of today, my site is available over a [secure connection][secure]. 18 | 19 | [secure]: https://ruudvanasseldonk.com/ 20 | -------------------------------------------------------------------------------- /posts/geomancer-at-indigo.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Geomancer at Indigo 3 | date: 2012-09-23 4 | lang: en-GB 5 | synopsis: One of the games that I worked on will be presented at Indigo 2012. 6 | run-in: During the past 7 | --- 8 | 9 | During the past half year at [IGAD](http://made.nhtv.nl/), 10 | I and my team have built the game _Geomancer_, 11 | a chess-like game which is played by altering 12 | the game world. The game will be presented at the Dutch game festival 13 | [Indigo](http://dutchgamegarden.nl/indigo/editions/2012/geomancer/) 14 | next week. 15 | 16 | ![(an overview of Geomancer)](/images/geomancer-overview.jpg) 17 | 18 | Indigo is an annual event organised by the 19 | [Dutch Game Garden](http://dutchgamegarden.nl), 20 | where game developers can showcase their games. 21 | Our team has been allowed to showcase Geomancer here. 22 | 23 | ![(a picture of a soldier under attack by a mage)](/images/geomancer-soldier-under-attack-by-mage.jpg) 24 | 25 | Geomancer -- the result of fourteen days of gamelab -- 26 | is a strategic game for two players. Characters can 27 | modify the terrain by moving hexagons up or down. 28 | This will damage hostile characters, while characters 29 | will be more gentle to their own team. By building 30 | staircases and walls, the opponent is forced to 31 | think ahead, and choose his moves strategically. 32 | 33 | ![(a picture of a mage in move mode)](/images/geomancer-move-mage.jpg) 34 | -------------------------------------------------------------------------------- /posts/git-music.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Git music 3 | date: 2013-07-20 4 | lang: en-GB 5 | synopsis: I cross-referenced my Git commit history with my Last.fm scrobbles. 6 | run-in: A while ago 7 | --- 8 | 9 | A while ago I wondered: would there be a correlation between the music I 10 | listen to, and the code I write? Do my listening habbits while 11 | programming differ from my regular listening habbits? Am I more 12 | productive while listening to a certain artist? 13 | 14 | This should be easy enough to find out: Git knows when I have been 15 | programming, and [Last.fm](http://last.fm) knows which tracks I have 16 | been listening to. All that was left to do, was cross-reference the 17 | data. 18 | 19 | GitScrobbler 20 | ------------ 21 | 22 | The next day, I hacked together 23 | [GitScrobbler](https://github.com/ruuda/gitscrobbler). It is a 24 | simple command-line program to which you pipe formatted `git log` 25 | output. It then retrieves the track that you listened to at the moment 26 | of the commit, using the Last.fm API. (An API-key is required to use the 27 | program.) When a commit is matched with a track, it prints the commit 28 | message along with the track title and artist. At the end, it prints 29 | some statistics like your top ten artists and the tracks most often 30 | committed to. 31 | 32 | For me, the results were not as exciting as I had hoped. My top artists 33 | looked very much like my regular top artists (to within statistical 34 | fluctuations), but there definitely are a few tracks that I commit to 35 | more often than other tracks. The rest of the list again matched my 36 | regular listening habbits pretty well. 37 | 38 | -------------------------------------------------------------------------------- /posts/gits-push-url.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Git’s push url 3 | date: 2016-05-10 4 | minutes: 1 5 | lang: en-GB 6 | synopsis: Here’s a little-known feature of Git that allows you to fetch over https but push with SSH. 7 | run-in: Fetching from 8 | --- 9 | 10 | Fetching from a public Git repository over https is convenient 11 | because it does not require any credentials. 12 | But to push, I’d rather use SSH. 13 | This means setting the remote url to the SSH one for repositories that I have push access to. 14 | I rarely use an SSH agent that can handle ed25519 keys, 15 | and unfortunately that means I have to unlock my key every time I pull or fetch, 16 | even when authentication is not required. 17 | 18 | A little-known feature of Git comes in handy here: [`remote.pushurl`][pushurl-docs]. 19 | To change the push url for a repository, 20 | run `git config -e` to open up the repository configuration in an editor. 21 | Every remote has a section here: 22 | 23 | ```ini 24 | [remote "github"] 25 | url = git@github.com:ruuda/blog 26 | fetch = +refs/heads/*:refs/remotes/github/* 27 | ``` 28 | 29 | The single url there is used both for pushing and fetching. 30 | To fetch via https but push over SSH, 31 | change it as follows: 32 | 33 | ```ini 34 | [remote "github"] 35 | pushurl = git@github.com:ruuda/blog 36 | url = https://github.com/ruuda/blog 37 | fetch = +refs/heads/*:refs/remotes/github/* 38 | ``` 39 | 40 | The fact that those urls line up so nicely makes me happier than it should. 41 | 42 | [pushurl-docs]: https://git-scm.com/docs/git-push#_named_remote_in_configuration_file 43 | -------------------------------------------------------------------------------- /posts/global-game-jam-2012.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Global Game Jam 2012 3 | date: 2012-01-30 4 | lang: en-GB 5 | synopsis: During the Global Game Jam 2012 I made an award-winning game about a particle accelerator. 6 | run-in: Last weekend 7 | --- 8 | 9 | Last weekend I participated in the [Global Game Jam](http://globalgamejam.org). 10 | In 48 hours, we made a game about a particle accelerator. 11 | With our game, _The Small Bang Theory_, we won the first prize of 12 | [our location](http://globalgamejam.org/sites/2012/ggjnl-nhtv-breda-university-applied-sciences), 13 | and we have a chance of winning the national prize as well. 14 | 15 | ![(picture of the game)](/images/the-small-bang-theory.jpg) 16 | 17 | Due to a lack of artists, our team made a game which is all procedural. 18 | The graphics are procedural, and there are infinitely many levels. 19 | (Although you certainly deserve a honourable mention if you make it past level six ...) 20 | The arcade-style game is available at 21 | [the game jam site](http://globalgamejam.org/2012/small-bang-theory "The Small Bang Theory"). 22 | -------------------------------------------------------------------------------- /posts/global-game-jam-2014.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Global Game Jam 2014 3 | date: 2014-01-28 4 | lang: en-GB 5 | synopsis: During the Global Game Jam 2014 I used Unity to build a game in 48 hours. 6 | run-in: Last weekend 7 | --- 8 | 9 | Last weekend the [Global Game Jam](http://globalgamejam.org) took place. 10 | The theme this time was “We don’t see things as they are, we see them as we are.” 11 | I joined the jam site in Breda, 12 | and in 48 hours we built the game _Tristar_. 13 | We took the theme quite literally, 14 | and built a game world that looks different to each of the three players. 15 | 16 | ![(picture of cyan’s point of view)](/images/tristar-cyan.png) 17 | 18 | Players play cyan, magenta or yellow, 19 | and only get to see their colour component of the world. 20 | At the location of the player, 21 | her colours ‘leak’ into the perception of the other players, 22 | revealing a part of her world. 23 | The objective of the game is to collect as many eggs as possible -- however, 24 | those eggs are generally not visible to the player, 25 | because they have the colours of the opponents. 26 | The player _can_ see the eggs of her opponents, 27 | and must not get too close to them, as not to reveal them to her opponents. 28 | This makes for some interesting gameplay, 29 | where you can chase the other players, hide and wait, 30 | or strategically reveal an egg to encourage other players to make a move. 31 | 32 | We [again](/2012/01/30/global-game-jam-2012) made it to the top-three with our game, 33 | the third place this time. 34 | 35 | This was my first time using [Unity](http://unity3d.com), 36 | the technology which the game is built with. 37 | It was perfect for rapid development like the game jam, 38 | because it has everything built-in. 39 | On the other hand, 40 | all the fun stuff is done for you already, 41 | and what is left to do is more like scripting than programming. 42 | That is fine for the game jam of course -- you want to have something playable in two days. 43 | A game jam is not the place to apply those dirty hacks to squeeze out every bit of processing power of your machine. 44 | Making the game was an awesome experience nonetheless; 45 | [this](http://youtu.be/OZBWfyYtYQY) playing at the beamer for at least half an hour says enough about the atmosphere, I think. 46 | 47 | The game and source code are available at [the game jam site](http://globalgamejam.org/2014/games/tristar). 48 | Check out the other games as well, 49 | it is amazing to see how many great concepts can be developed in so little time. 50 | -------------------------------------------------------------------------------- /posts/global-game-jam-2015.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Global Game Jam 2015 3 | date: 2015-02-05 4 | lang: en-GB 5 | synopsis: This year’s Global Game Jam we built our most beautiful gam jam game yet. 6 | run-in: It is starting 7 | --- 8 | 9 | It is starting to become a tradition: 10 | jamming with the [global game jam](http://globalgamejam.org) at the Breda jam site. 11 | In just 48 hours, we built the game [_Richy’s Groceries_](http://globalgamejam.org/2015/games/richy’s-groceries) around the theme _What do we do now?_ 12 | In the game, the player assumes the role of a mother shopping for groceries with her daughter 13 | when the store is about to close. 14 | 15 | ![Richy’s Groceries](/images/richys-groceries.jpg) 16 | 17 | I had a lot of fun during the weekend, and I’m very pleased with the result. 18 | I especially like the distinctive visual style. 19 | This is without doubt the most beautiful game jam game that I ever worked on. 20 | It is really impressive what Derk Over and Hans Crefcoeur 21 | -- the two artists on our team -- 22 | managed to pull off in such a short amount of time. 23 | This year Maarten Crefcoeur joined us for audio, 24 | so the game features original music as well. 25 | 26 | For the 27 | [third](/2012/01/30/global-game-jam-2012) 28 | [year](/2014/01/28/global-game-jam-2014) 29 | we made it into the top three with our game, 30 | finishing third out of the 32 games that were built in Breda this year. 31 | If you would like to play the game, 32 | you can grab the Windows or Android binaries from [GitHub](https://github.com/ruuda/ggj15/releases). 33 | 34 | We used [Unity](http://unity3d.com) once more, 35 | which is a great environment for building a game quickly without bothering too much with boilerplate. 36 | I also brought my tablet this year. 37 | Exporting for Android from Unity is really no more difficult than building a Windows binary, 38 | and with just a few extra lines of code for touch input, 39 | we got the game running on the tablet as well. 40 | -------------------------------------------------------------------------------- /posts/llm-interactions.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: LLM text makes human interactions less fun 3 | date: 2025-01-12 4 | lang: en-US 5 | synopsis: Building connections with other people is what makes life fun. When humans communicate through LLM-written text, we lose that. 6 | minutes: 3 7 | run-in: All humans are unique. 8 | teaser: ai-alignment-starter-pack 9 | --- 10 | 11 | All humans are unique. 12 | We look different, 13 | have different voices, 14 | different mannerisms, 15 | different fashion preferences, 16 | different ways of speaking, 17 | and different personalities. 18 | Part of what makes life fun, 19 | is getting to know those other humans, 20 | and building deeper connections with them. 21 | 22 | LLMs are, in one sense, all the same. 23 | Everybody uses the same foundation models, 24 | and the handful that are there, 25 | are trained in a way that makes their default writing styles very similar. 26 | 27 | I have a remote job in tech. 28 | This means that a large part of my interactions with other humans happens online, 29 | through text. 30 | In text, the things that set us apart are stripped away. 31 | You don’t hear voices or intonation, 32 | you don’t see faces and body language. 33 | We may get to pick a tiny picture as avatar, 34 | but even the fonts are uniform. 35 | The one thing that remains in text 36 | — the last trace of personality that cannot be stripped away — 37 | is writing style. 38 | 39 | Learning to recognize somebody’s writing style 40 | is part of getting to know that person, 41 | just like learning what their taste in food or music is. 42 | With people I know well, 43 | I can often identify the author by the writing style 44 | in natural languages that I’m fluent in, 45 | and sometimes even in code. 46 | That we express ourselves in different ways, 47 | is what makes interacting with other people fun! 48 | 49 | When people start using LLMs to inflate text for them, 50 | or to rewrite text to sound more professional or eloquent, 51 | that last trace of personality is lost. 52 | I can often spot LLM use by the writing style, 53 | and it makes me sad when real humans do that. 54 | It’s the equivalent of turning off your camera in a video call: 55 | hiding behind uniformity, erasing personality. 56 | 57 | When LLMs became popular, 58 | I initially thought they could bring productivity gains, 59 | and help less advanced speakers express themselves. 60 | But as LLMs get integrated into our daily lives, 61 | LLM-generated text starts to feel more like a double insult: 62 | it says “I couldn’t be bothered to spend time writing this myself,” 63 | yet it makes _me_ waste time reading through the generated fluff! 64 | It would be more respectful of each other’s time 65 | to just share the prompt directly. 66 | As for helping less advanced speakers, 67 | I now realize that I much prefer to read broken English, 68 | over reading something that sounds like AI slop. 69 | Because then, at least I feel like I’m interacting with a _person_! 70 | 71 | All of this is not to say that LLMs are not useful, 72 | or that they can’t impersonate different writing styles when prompted. 73 | It’s just that when I’m interacting with other people, 74 | I like to interact with those people directly, 75 | and not through an LLM filter that strips away personality, 76 | and removes the joy from human interactions. 77 | -------------------------------------------------------------------------------- /posts/model-facts-not-your-problem-domain.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Model facts, not your problem domain 3 | date: 2020-06-07 4 | minutes: 1 5 | lang: en-GB 6 | synopsis: When requirements change, an append-only data model of immutable facts is more useful than a mutable data model that models the problem domain. 7 | run-in: When requirements change 8 | teaser: please-put-units-in-names 9 | --- 10 | 11 | When requirements change, 12 | an append-only data model of immutable facts is more useful 13 | than a mutable data model that models the problem domain. 14 | 15 | When I started building [a plant watering tracker](https://github.com/ruuda/sempervivum) 16 | about six weeks ago, 17 | I almost made the mistake of giving its database a mutable ‘last watered’ column. 18 | Fortunately I realised in time to use an append-only table with watering events. 19 | At the time I had no use for historical data, 20 | the only things required for a watering reminder 21 | are the watering interval and last watered date. 22 | When mutability is the default, 23 | storing all watering events seems ridiculous. 24 | But when immutable is your default, 25 | keeping historical data is the natural thing to do: 26 | mutating rows in place is a storage space optimisation, 27 | and in my case it would be entirely premature. 28 | 29 | Now, a few weeks later, I want to make the watering schedule adaptive. 30 | If the watering reminder is consistently two days early, 31 | probably the default interval is too short for that plant. 32 | For this feature I do need historical data. 33 | Fortunately, I already have that data, 34 | so the new feature is useful right away. 35 | But more importantly, 36 | no schema changes are required to support the new feature. 37 | 38 | My point is not “collect all the things, it might be useful some day.” 39 | My point is that a database that stores facts (watering events) 40 | is in a much better position to deal with changing requirements, 41 | than a database that was modelled after the initial requirements 42 | (producing a reminder a fixed number of days after watering). 43 | When requirements change, 44 | the facts of the situation do not, 45 | so a database of facts remains useful. 46 | 47 | Further reading 48 | --------------- 49 | 50 | My preference for immutable data is heavily informed by the work of Rich Hickey, 51 | who explains this much better than I ever could in a short blog post. 52 | The following talks are worth watching: 53 | 54 | * [The Value of Values](https://www.infoq.com/presentations/Value-Values/) 55 | * [Simple Made Easy](https://www.infoq.com/presentations/Simple-Made-Easy/) 56 | * [Deconstructing the Database](https://www.infoq.com/presentations/Deconstructing-Database/) 57 | -------------------------------------------------------------------------------- /posts/neither-necessary-nor-sufficient.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Neither necessary nor sufficient 3 | break: necessary nor 4 | date: 2015-10-06 5 | minutes: 5 6 | lang: en-GB 7 | synopsis: A garbage collector is neither necessary nor sufficient for quality software because it does not solve the problem of resource management. 8 | run-in: A few days ago 9 | teaser: zero-cost-abstractions 10 | --- 11 | 12 | A few days ago I stumbled upon a [blog post][when-rust-makes-sense] 13 | that raised the following question: 14 | 15 | > “A language without garbage collection, in 2015?” 16 | 17 | The language referred to is Rust, 18 | but that is hardly relevant here. 19 | I wrote a reply on Reddit, 20 | but I thought I’d take the time to elaborate a bit more 21 | in the form of a blog post. 22 | Quoting [Bjarne Stroustrup][bjarne-quote] 23 | the point can be summarised succinctly: 24 | 25 | > “Garbage collection is neither necessary nor sufficient.” 26 | 27 | [when-rust-makes-sense]: https://m50d.github.io/2015/09/28/when-rust-makes-sense.html 28 | [bjarne-quote]: https://isocpp.org/blog/2015/09/bjarne-stroustrup-announces-cpp-core-guidelines 29 | 30 | A language without garbage collection, in 2015. 31 | Why? 32 | Because garbage collection does not solve the deeper underlying problem: 33 | _resource management_. 34 | Performance differences aside 35 | (there are many different metrics for performance 36 | and even more myths surrounding those 37 | -- I don’t want to go down that rabbit hole here), 38 | a garbage collector only manages memory. 39 | This works well for memory, 40 | because there generally is more memory available than what is needed, 41 | and applications do not care about the actual addresses they use. 42 | The address space is uniform. 43 | An array does not care if it is stored at `0x3a28213a` or `0x6339392c`. 44 | If something that is no longer alive was stored at `0x3a28213a`, 45 | the array is happy with being stored at `0x6339392c` 46 | if the garbage collector has not yet discovered that `0x3a28213a` is free. 47 | 48 | The story is different when there is contention for a resource. 49 | From the lock protecting a critical section to the socket serving a website 50 | -- you cannot afford to leave such resources lingering around 51 | until a GC comes along to decide what is still being used. 52 | In most languages sporting a GC, 53 | resource management is still utterly manual. 54 | You still need to `close()` your `OutputStream` in Java. 55 | You still need to `Release()`, `Close()` or `Dispose()` of your `Semaphore` in C#. 56 | Even this Haskell example straight from [Real World Haskell][real-world-haskell] 57 | is nothing more than fancy syntax for C’s file handles: 58 | 59 | ```haskell 60 | main = do 61 | inh <- openFile "input.txt" ReadMode 62 | outh <- openFile "output.txt" WriteMode 63 | mainloop inh outh 64 | hClose inh 65 | hClose outh 66 | ``` 67 | 68 | The problem with disposable objects or handles, 69 | is that they decouple resource lifetime from object lifetime. 70 | This allows for programming errors such as writing to a closed socket 71 | or not releasing a lock. 72 | 73 | There exist constructs that can help in many cases. 74 | Python has `with`, C# has `using`, and Haskell has `bracket`. 75 | These constructs bind resource lifetime to scope. 76 | A scope-based solution is often sufficient, 77 | but in some cases a resource has to outlive the current scope. 78 | A `using` block for instance, 79 | is of no use for disposable member variables. 80 | 81 | [real-world-haskell]: http://book.realworldhaskell.org/read/io.html#io.files 82 | 83 | Of course, sometimes manual control of resoures is required. 84 | A device driver _does_ care about actual addresses, 85 | and manual acquire and release calls might be clearer than scope-based locking 86 | for a thread-safe cache protected by a read-write lock. 87 | In the specialised cases where there is a need for manual resource management, 88 | neither garbage collection nor automatic resource mangement can help. 89 | But little code benefits from manual memory management, 90 | and I would argue that this is the case for other resourses as well. 91 | 92 | So yes, I want a language without garbage collection. 93 | Because I want a language that can do _resource management_. 94 | 95 | -------------------------------------------------------------------------------- /posts/one-year-with-colemak.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: One year with Colemak 3 | date: 2014-07-17 4 | lang: en-GB 5 | synopsis: After typing in Colemak for a year I am still convinced that it was worth the switch. 6 | run-in: About a year ago 7 | --- 8 | 9 | About a year ago I switched to [Colemak][colemak]. 10 | At the time I was experiencing a slight strain in my hands. 11 | I had bought a new [keyboard][kbd] already, 12 | but the strains persisted. 13 | I had heard about Colemak before, 14 | and a few months later a post about the keyboard layout made it to the front page of Hacker News. 15 | I decided to give it a try. 16 | 17 | Afraid to be completely inproductive for some time, 18 | I did not want to make the switch cold-turkey. 19 | I practiced Colemak in the evenings, and typed Qwerty during the day. 20 | After little more than a week, 21 | I got to a point where I started to make lots of mistakes typing Qwerty, 22 | but I could not type proper Colemak either. 23 | This was something I had not anticipated: 24 | I expected to learn Colemak _in addition to Qwerty_, 25 | not instead of it! 26 | Apparently my muscle memory can store only one keyboard layout. 27 | Maybe this is different for different people, but it was not something I expected. 28 | I decided to go through: Colemak full-time it was. 29 | 30 | At first I wasn’t very fast, 31 | but within a few days I could type at a reasonable speed again. 32 | Within a few weeks, I think reached a speed again where words per minute was not the bottleneck any more. 33 | After a few months, I think I could type as fast as I could type Qwerty, maybe even faster. 34 | I did not measure my typing speed before making the switch, 35 | so unfortunately I have no hard data. 36 | 37 | [kbd]: http://www.microsoft.com/hardware/en-us/p/natural-ergonomic-keyboard-4000/B2M-00012 38 | [colemak]: http://colemak.com/ 39 | 40 | What you gain 41 | ------------- 42 | Aside from a layout that is optimised for typing instead of not jamming typewriters, 43 | what does Colemak get you? 44 | For starters, it is a multilingual layout, and it puts useful punctuation right at your fingertips. 45 | Dutch requires accents and diaereses for some words, 46 | so the most common keyboard layout is US-international with dead keys. 47 | `e` after `"` becomes `ë`, `e` after `'` becomes `é`, etc. 48 | This means that you often need to type a space after a backtick or quote 49 | to prevent it from combining with the next character. 50 | Especially when programming, the quotes are far more frequent than accented characters, 51 | which is unfortunate. 52 | 53 | In Colemak, the most common characters are not behind dead keys. 54 | A quote is simply a quote. 55 | The `ë` is behind a dead key (`AltGr` + `d`, `e`), but the `é` is not (`AltGr` + `e`). 56 | Colemak uses `AltGr` for extra characters, and it makes a lot of sense. 57 | `AltGr` + `-` becomes an en-dash, and `Shift` + `AltGr` + `-` becomes an em-dash. 58 | These are characters that I use more often than e.g. the percent sign, 59 | so it is nice to have direct key combinations for them. 60 | Also, correct quotes can be typed directly: `AltGr` + `(` for `‘`, `AltGr` + `)` for `’`. 61 | Add shift to make it a double quote. 62 | 63 | I do not know for sure whether I can type Colemak significantly faster than Qwerty. 64 | For most things I type, speed is not the bottleneck anyway. 65 | I do know that Colemak is a lot easier on the fingers, 66 | for Dutch as well as English and code. 67 | 68 | The disadvantages 69 | ----------------- 70 | The main disadvantage of Colemak has nothing to do with Colemak: it is Qwerty. 71 | Qwerty is still pervasive, and you do not always get to choose your keyboard layout. 72 | Need to quickly look something up at the nearest computer? 73 | It will likely have a Qwerty keyboard. 74 | Want to help someone out? 75 | She will probably use a Qwerty layout. 76 | Sometimes you do get to choose the layout. 77 | At my university for example, I could set the layout to Colemak. 78 | I still need to use Qwerty to log in, 79 | but my passwords are either muscle memory or too hard to remember, 80 | so that is not an issue. 81 | Also, Colemak is not supported out of the box on Windows, 82 | so it might be more difficult there if you do not control the machine. 83 | 84 | An other aspect of the pervasiveness of Qwerty appears in key bindings. 85 | Qwerty is assumed everywhere. 86 | Want to walk around in a shooter? 87 | `wsad` suddenly became a very awkward way of navigating. 88 | `hjkl` in Vim? 89 | These are not all on the home row any more. 90 | Of course, all of this can be remapped. 91 | For the shooter it is easy, 92 | but for many applications it is a balance between a sane layout and sane meaning. 93 | I did not re-map any keys in Vim 94 | (except mapping caps lock to escape, but I did that with Qwerty as well). 95 | 96 | Was it worth it? 97 | ---------------- 98 | In the end, I think it was worth the switch. 99 | Colemak is a much nicer layout to type, 100 | and it is especially nice to have keys for the correct dashes and quotes. 101 | When you need to work with a Qwerty keyboard, 102 | you can always fall back to non-blind typing. 103 | For me that is more than an order of magnitude slower, 104 | but it is rare enough. 105 | I will continue to use Colemak for the foreseeable future. 106 | -------------------------------------------------------------------------------- /posts/passphrase-entropy.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Passphrase entropy 3 | date: 2012-07-07 4 | lang: en-GB 5 | synopsis: Using longer encryption keys does not improve security unless you increase the length of your password too. 6 | run-in: While I was researching 7 | --- 8 | 9 | While I was researching passphrase entropy today, I made a shocking discovery. 10 | _Virtually any passphrase that a human can remember, 11 | does not have enough entropy to generate a strong cryptographic key._ 12 | Even though most modern encryption algorithms use key sizes of 128 or 256 bits, 13 | a comparatively ‘strong’ passphrase has nowhere near this amount 14 | of entropy bits. 15 | 16 | Entropy 17 | ------- 18 | Entropy in information theory, is related to the number of possible values 19 | of a variable. The more possibilities, the more entropy. 20 | Entropy makes it difficult to do a brute-force attack, and it plays a role 21 | in several other types of attack. For a brute-force attack, the relation 22 | is easy to see: if a variable can have many different values, it will 23 | require much work to try all possible values. If the variable can take on 24 | only a limited amount of values, trying them all might not require 25 | that much work. Entropy is a desirable property of cryptographic keys. 26 | 27 | The power of encryption lies in the large keyspace (the set of possible 28 | keys). Probably the most common algorithm nowadays is Rijndael with a 256-bit key. 29 | Brute-forcing a 256-bit key is simply 30 | [infeasible](http://crypto.stackexchange.com/questions/1145/how-much-would-it-cost-in-u-s-dollars-to-brute-force-a-256-bit-key-in-a-year), 31 | so a 256-bit key can be considered secure. However, this is 32 | only true if the key is random -- that is, if the key 33 | has 256 bits of entropy. 34 | 35 | Deriving keys 36 | ------------- 37 | In many cases, the key cannot be stored. It must be remembered by a human, 38 | so data cannot be decrypted without this person’s knowledge. Unfortunately, 39 | humans are not every good at remembering a sequence of 256 bits. 40 | Even when expressed octally, it is still a sequence of 64 random digits. 41 | This is where key derivation comes into play. A key derivation algorithm 42 | generates a cryptographic key, given a passphrase. This way, a human 43 | can simply remember the passphrase, 44 | and the key can be generated wherever it is required. 45 | Even though several key derivation algorithms can convert passphrases of 46 | arbitrary length to arbitrary length keys, this does not mean that 47 | _entropy_ increases. If a passphrase would be limited to five different 48 | words, there would be only five different keys. A key derivation algorithm 49 | cannot directly increase the entropy of a passphrase. 50 | 51 | Typical passphrase entropy 52 | -------------------------- 53 | As suggested by [xkcd](https://xkcd.com/936/), one can use a combination 54 | of four common words to make up a ‘strong’ passphrase. The comic assumes 55 | entropy of eleven bits per word, resulting in 44 bits of entropy 56 | for the entire passphrase. To see how this number is derived, 57 | we must adjust our view on the brute-force attack. As it is infeasible 58 | to brute-force all 2256 keys, a different strategy is required. 59 | If we assume that all passphrases are combinations of four words, 60 | then we can simply try all possible combinations of four words as a 61 | brute-force attack. To do so, one needs a dictionary of words. 62 | The _Van Dale Pocket Dictionary English - Dutch_ contains roughly 23000 words, 63 | which should include the most common English words. If we assume that only 64 | words from this dictionary can be used for passphrases, and we start with 65 | one-word passphrases, then there are 23000 unique options to try. 66 | The number of bits required to identify a value among 23000 possibilities, 67 | is log2(23000) ≈ 14.5 bits. (The comic assumes eleven bits per word, 68 | so it must have used a smaller dictionary.) For four words, 69 | the entropy is four times as large: 58 bits (or 44 in the comic). 70 | 71 | It is possible to try some variations on the four-word scheme, 72 | but this does not add a significant abount of entropy. For example, 73 | making the first character of every word either uppercase or lowercase 74 | (instead of solely lowercase), adds only one bit of entropy per word. 75 | Appending a random number between zero and a thousand, 76 | adds only ten bits of entropy. 77 | 78 | Key stretching 79 | -------------- 80 | As it turns out, some key derivation algorithms _can_ be used to increase 81 | the entropy of a key. Most key derivation algorithms run a large number 82 | of iterations, which makes the derivation computationally expensive. 83 | Brute-forcing a certain amount of keys, is computationally expensive as well. 84 | [Key stretching](https://www.schneier.com/paper-low-entropy.html) 85 | relies on the principle that deriving the key in an expensive way, 86 | requires an amount of work equal to brute-forcing a certain number of keys. 87 | If a key of ten bits can be brute-forced in five seconds, then requiring 88 | the key derivation to take five seconds effectively increases the entropy of the key 89 | with ten bits. This technique can be used to increase the entropy 90 | of a key with at most a few dozen bits, but still 91 | -- that is nowhere near the 256 available bits. 92 | 93 | Solutions 94 | --------- 95 | I have not found a good solution to this problem yet. 96 | (If you have, feel free to share it with me.) 97 | The most obvious solution is to have an alternative source of entropy; a keyfile. 98 | However, the keyfile has several other problems that make its source of entropy rather useless. 99 | The keyfile must be stored, which is a major problem in itself. 100 | After all, keyfiles only make it harder for an attacker to find the key. 101 | In the end -- even if you have a perfectly random key -- 102 | there are always [alternative ways](https://xkcd.com/538/) for an attacker 103 | to break your encryption. 104 | -------------------------------------------------------------------------------- /posts/please-put-units-in-names.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Please put units in names 3 | break: put units 4 | subheader: or use strong types 5 | date: 2022-03-20 6 | lang: en-US 7 | minutes: 4 8 | synopsis: Using strong types, or putting units in names, is a small effort that can make a tremendous difference for code readability. 9 | run-in: There is one 10 | --- 11 | 12 | There is one code readability trap 13 | that is easy to avoid once you are aware of it, 14 | yet the trap is pervasive: omitting units. 15 | Consider the following three snippets in Python, Java, and Haskell: 16 | 17 | ```python 18 | time.sleep(300) 19 | ``` 20 | ```java 21 | Thread.sleep(300) 22 | ``` 23 | ```haskell 24 | threadDelay 300 25 | ``` 26 | 27 | How long do these programs sleep for? 28 | The Python program sleeps for five minutes, 29 | the Java program sleeps for 0.3 seconds, 30 | and the Haskell program sleeps for 0.3 milliseconds. 31 | 32 | How can you tell this from the code? 33 | You can’t. 34 | You just have to know by heart that `time.sleep` takes seconds, 35 | while `threadDelay` takes microseconds. 36 | If you look it up often enough, 37 | eventually that knowledge will stick, 38 | but how can we keep the code readable 39 | even for people who haven’t encountered `time.sleep` before? 40 | 41 | Option 1: put the unit in the name 42 | ---------------------------------- 43 | 44 | Instead of this: 45 | ```python 46 | def frobnicate(timeout: int) -> None: 47 | ... 48 | 49 | frobnicate(300) 50 | ``` 51 | 52 | Do this: 53 | ```python 54 | def frobnicate(*, timeout_seconds: int) -> None: 55 | # The * forces the caller to use named arguments 56 | # for all arguments after the *. 57 | ... 58 | 59 | frobnicate(timeout_seconds=300) 60 | ``` 61 | 62 | In the first case, we can’t even tell at the call site that 300 is a timeout, 63 | but even if we knew that, it’s a timeout of 300 what? Milliseconds? Seconds? 64 | Martian days? In contrast, the second call site is completely self-explanatory. 65 | 66 | Using named arguments is nice for languages that support it, 67 | but this is not always a possibility. 68 | Even in Python, where `time.sleep` is defined with a single argument named `secs`, 69 | we can’t call `sleep(secs=300)` due to implementation reasons. 70 | In that case, we can give the value a name instead. 71 | 72 | Instead of this: 73 | 74 | ```python 75 | time.sleep(300) 76 | ``` 77 | 78 | Do this: 79 | 80 | ```python 81 | sleep_seconds = 300 82 | time.sleep(sleep_seconds) 83 | ``` 84 | 85 | Now the code is unambiguous, 86 | and readable without having to consult the documentation. 87 | 88 | Option 2: use strong types 89 | -------------------------- 90 | 91 | An alternative to putting the unit in the name, 92 | is to use stronger types than integers or floats. 93 | For example, we might use a duration type. 94 | 95 | Instead of this: 96 | ```python 97 | def frobnicate(timeout: int) -> None: 98 | ... 99 | 100 | frobnicate(300) 101 | ``` 102 | 103 | Do this: 104 | ```python 105 | def frobnicate(timeout: timedelta) -> None: 106 | ... 107 | 108 | timeout = timedelta(seconds=300) 109 | frobnicate(timeout) 110 | ``` 111 | 112 | For a given floating-point number, 113 | you need to be told out of band what the unit is to be able to interpret it. 114 | If you are lucky this information is in the variable or argument name, 115 | but if you are unlucky it’s only specified in the documentation 116 | — or not specified at all. 117 | But for a `timedelta` value, 118 | there is no ambiguity about how to interpret it, 119 | this is part of the type. 120 | This also removes the ambiguity from the code. 121 | 122 | Scope 123 | ----- 124 | 125 | The advice to use strong types or to put units in names 126 | is not limited to variables and function arguments, 127 | it’s applicable to API s, 128 | [metric names](https://prometheus.io/docs/practices/naming/#metric-names), 129 | serialization formats, configuration files, command-line flags, etc. 130 | And although duration values are the most common case, 131 | this advice is not limited to those either, 132 | it also applies to monetary amounts, lengths, data sizes, etc. 133 | 134 | For example, don’t return this: 135 | ```json 136 | { 137 | "error_code": "E429", 138 | "error_message": "Rate limit exceeded", 139 | "retry_after": 100, 140 | } 141 | ``` 142 | 143 | Return this instead: 144 | ```json 145 | { 146 | "error_code": "E429", 147 | "error_message": "Rate limit exceeded", 148 | "retry_after_seconds": 100, 149 | } 150 | ``` 151 | 152 | Don’t design your config file like this: 153 | ``` 154 | request_timeout = 10 155 | ``` 156 | 157 | Accept one of these instead: 158 | ``` 159 | request_timeout = 10s 160 | request_timeout_seconds = 10 161 | ``` 162 | 163 | And don’t design your CLI accounting app like this: 164 | ``` 165 | show-transactions --minimum-amount 32 166 | ``` 167 | 168 | Accept one of these instead: 169 | ``` 170 | show-transactions --minimum-amount-eur 32 171 | show-transactions --minimum-amount "32 EUR" 172 | ``` 173 | -------------------------------------------------------------------------------- /posts/the-small-bang-theory-in-san-francisco.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: The Small Bang Theory in San Francisco 3 | break: Theory in 4 | date: 2013-03-18 5 | lang: en-GB 6 | synopsis: A game that I worked on will be on display during the Game Developers Conference next week. 7 | --- 8 | 9 | During the Game Developers Conference next week, 10 | the Dutch Game Garden will host [INDIGO @ GDC](http://dutchgamegarden.nl/indigo/editions/gdc2013). 11 | One of the games on display there, will be _The Small Bang Theory_, 12 | the award-winning game I and my team developed during the Global Game Jam 2012. 13 | If you happen to be in San Francisco between 25 and 30 March, 14 | be sure to drop by the Dutch Consulate! 15 | 16 | -------------------------------------------------------------------------------- /posts/the-task-monad-in-csharp.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: The task monad in C# 3 | date: 2013-05-01 4 | lang: en-GB 5 | synopsis: When you are composing asynchronous code, you are using monads. This posts reveals the form the task monad takes in C#. 6 | run-in: Today I was trying 7 | --- 8 | 9 | Today I was trying to compose several asynchronous methods in C#. 10 | These methods all had one argument, and returned a `Task`. 11 | The idea was very simple: the first method returns a `Task`. 12 | The second method takes an `A` and returns a `Task`. 13 | The third method takes a `B`, etc. As it turns out, there is no easy 14 | way to compose these methods in C#, even though there should be. 15 | 16 | The `System.Threading.Tasks` namespace is very well designed, and overall 17 | everything seems to be thought through very well. However, in this 18 | particular case, the framework seems to lack an obvious composition operation. 19 | Consider the following scenario: 20 | 21 | ```cs 22 | class A { ... } 23 | class B { ... } 24 | class C { ... } 25 | class D { ... } 26 | 27 | Task Method1(A a) { ... } 28 | Task Method2(B b) { ... } 29 | Task Method3(C c) { ... } 30 | ``` 31 | 32 | I would like to compose these methods, to create a method with the following 33 | signature: 34 | 35 | ```cs 36 | Task Composed(A a) { ... } 37 | ``` 38 | 39 | Before tackling the general case of _n_ methods, consider composing 40 | `Method1` and `Method2`, to end up with the method: 41 | 42 | ```cs 43 | Task Composed12(A a) { ... } 44 | ``` 45 | 46 | Using the `ContinueWith` method, a naive composition operation might be 47 | as follows: 48 | 49 | ```cs 50 | Task<...> Composed12(A a) 51 | { 52 | return Method1(a).ContinueWith(tb => Method2(tb.Result)); 53 | } 54 | ``` 55 | 56 | This works, but the method has the wrong return type. The result is now 57 | wrapped twice in a task. To solve this issue, one might write: 58 | 59 | ```cs 60 | Task Composed12(A a) 61 | { 62 | return Method1(a).ContinueWith(tb => Method2(tb.Result).Result); 63 | } 64 | ``` 65 | 66 | However, this is not really a continuation: it blocks. The overall result 67 | is the same, but as soon as the task returned by `Method1` is completed, 68 | the continuation task will block, waiting for the result of `Method2`. 69 | This essentially wastes a thread. One could use `Unwrap`, which does solve the 70 | problem in many cases, but not when a task fails or when it is cancelled. 71 | 72 | Bind 73 | ---- 74 | If you have ever done some functional programming, you might recognise 75 | something here. `Task` is a **monad**, and the composition operation I am 76 | looking for is the monadic composition operation (usually called _bind_). 77 | Given two methods, one that returns a `Task`, and one that takes a `B` 78 | and returns a `Task`, composition should give me a method that returns a `Task`. 79 | This is exactly what bind does. In the context of tasks, 80 | the name ‘bind’ might seem a bit odd. A more natural name in this case, 81 | would be ‘then’. Peculiar enough, the designers of `Task` did not include such 82 | a method. Fortunately, [implementations of this method](https://blogs.msdn.com/b/pfxteam/archive/2010/11/21/10094564.aspx) 83 | have been written. Using this `Then` method, composition becomes easy and elegant: 84 | 85 | ```cs 86 | Task Composed12(A a) 87 | { 88 | return Method1(a).Then(Method2); 89 | } 90 | 91 | Task Composed(A a) 92 | { 93 | return Method1(a).Then(Method2).Then(Method3); 94 | } 95 | ``` 96 | 97 | The default choice of continuing the task synchronously might not always be 98 | desired, but this can easily be changed. 99 | 100 | Return 101 | ------ 102 | For `Task` to be a monad, it not only requires a ‘bind’ method, 103 | but a ‘return’ method as well. The return method is nothing like the `return` 104 | statement in C#. What it does, is convert a non-monadic value, into a 105 | monadic value. In other words, it converts `T` into a `Task`. The .NET 106 | framework 4.5 implements return: it is called `Task.FromResult`. For 107 | earlier versions, a manual implementation is very simple: 108 | 109 | ```cs 110 | Task Return(T t) 111 | { 112 | var tcs = new TaskCompletionSource(); 113 | tcs.SetResult(t); 114 | return tcs.Task; 115 | } 116 | ``` 117 | 118 | In the case of my composed method above, this would allow one to write: 119 | 120 | ```cs 121 | Task Composed(A a) 122 | { 123 | return Return(a).Then(Method1).Then(Method2).Then(Method3); 124 | } 125 | ``` 126 | 127 | As you can see, this has little advantage over the original form. Moreover, 128 | when the synchronous continuation implementation of `Then` is used, 129 | this will block! There might still be cases where return could be useful though. 130 | -------------------------------------------------------------------------------- /posts/working-on-a-virtualenv-without-magic.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Working on a virtualenv without magic 3 | date: 2016-10-02 4 | lang: en-GB 5 | minutes: 3 6 | synopsis: The standard way of activating a Python virtualenv has a number of issues. Here is a simpler, understandable method. 7 | run-in: To keep installed dependencies 8 | --- 9 | 10 | To keep installed dependencies nice and isolated, 11 | Python programmers run their code inside a virtualenv. 12 | Unfortunately virtualenvs can be a bit hairy. 13 | They require you to *source* an 80-line shell script (eww), 14 | and you need to locate the activate script manually inside the virutalenv. 15 | Fortunately, there is [Virtualenvwrapper][venvwrapper]. 16 | After all, any problem in computer science can be solved with another layer of indirection. 17 | Among other things, it provides a handy `workon` command to activate a virtualenv. 18 | But do we really *need* any of this? 19 | And what is actually going on? 20 | 21 | I don’t like tools that I don’t understand. 22 | That is not to say that I am wary of tools that hide complexity, 23 | as long as I know *what* they are hiding from me. 24 | Frustrated by the virtualenv magic, 25 | I decided to investigate. 26 | And surely enough, I was not the only one who felt like this. 27 | Michael F. Lamb has [a great writeup][inve-gist] about what virtualenv actually does, 28 | and how to do that in a better way. 29 | Instead of repeating that, 30 | I’ll share the solution that I derived from it here. 31 | 32 | [venvwrapper]: https://virtualenvwrapper.readthedocs.io/en/latest/index.html 33 | [inve-gist]: https://gist.github.com/datagrok/2199506 34 | 35 | A simpler workon 36 | ---------------- 37 | 38 | It turns out that the only thing required to get the virtualenv working, 39 | is to set and unset a few environment variables. 40 | That shouldn’t be too hard. 41 | The next thing to do is to start a new shell with this environment, 42 | so you can get out of the virtualenv simply with `exit` -- not some ad-hoc `deactivate` command. 43 | I put this all together in a `workon` function defined in my shell startup script: 44 | 45 | ```sh 46 | # A function to activate a virtualenv without sourcing madness. 47 | # Based on https://gist.github.com/datagrok/2199506. 48 | function workon { 49 | export VIRTUAL_ENV="$HOME/env/$1" 50 | export PATH="$VIRTUAL_ENV/bin:$PATH" 51 | unset PYTHON_HOME 52 | $SHELL 53 | } 54 | ``` 55 | 56 | It behaves similar to Virtualenvwrapper: 57 | `workon foo` will activate the `~/env/foo` virtualenv. 58 | Note that I am storing my virtualenvs in `~/env`. 59 | 60 | Adding tab completion 61 | --------------------- 62 | 63 | I quickly grew tired of not having tab completion for my custom `workon`, 64 | so I added the following Zsh completion definition right after the function: 65 | 66 | ```sh 67 | # Autocomplete the "workon" command with directories in ~/env. 68 | compdef '_path_files -/ -g "$HOME/env/*" -W "$HOME/env/"' workon 69 | ``` 70 | 71 | Here `_path_files` activates filename completion. 72 | The `-/` flag specifies that only directories should be completed, 73 | and `-g` specifies that only directories matching the given pattern should be suggested. 74 | Finally, the `-W` flag indicates that the prefix `$HOME/env/` 75 | should be stripped from the suggested paths. 76 | 77 | Decorating the prompt 78 | --------------------- 79 | 80 | One thing the `activate` script does in addition to setting environment variables, 81 | is putting the name of the virtualenv in the promt. 82 | In the process it messes up the newlines in my prompt, 83 | and I can’t stand the lack of a space after the virtualenv name. 84 | I am glad to have gotten rid of this “feature”, 85 | although occasionally it can be useful to know whether you are inside a virtualenv. 86 | Putting it in the prompt is a nice indicator. 87 | Fortunately that is not hard to do at all: 88 | 89 | ```sh 90 | if [ -n "$VIRTUAL_ENV" ]; then 91 | VENV_PROMPT=" $(basename $VIRTUAL_ENV)" 92 | else 93 | VENV_PROMPT="" 94 | fi 95 | ``` 96 | 97 | Inside a virtualenv, `$VENV_PROMPT` will be the name of the virtualenv, prefixed by a space. 98 | Outside of a virtualenv it will be empty. 99 | This variable can then be used in the prompt however you like. 100 | -------------------------------------------------------------------------------- /posts/writing-a-path-tracer-in-rust-part-1.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Writing a path tracer in Rust, part 1 3 | header: Writing a path tracer in Rust 4 | break: path tracer 5 | part: 1 6 | lang: en-GB 7 | date: 2014-08-10 8 | minutes: 9 9 | synopsis: As a learning exercise, I am going to port my spectral path tracer Luculentus from C++ to Rust. 10 | run-in: It must have been 11 | --- 12 | 13 | It must have been almost a year ago that I first heared about [Rust][rust]. 14 | Posts about the language would appear on [/r/programming][reddit] now and then, 15 | and the language caught my attention. 16 | Rust got me excited for serveral reasons: 17 | 18 | - One of the key points of Rust is a very powerful way to do deterministic resource management. 19 | This is a weak point of the languages that I use most often (C++ and C# nowadays). 20 | - Rust is a systems language that offers zero-cost abstractions. 21 | - Rust offers a very powerful type system with constructs that you would normally find in a functional language. 22 | 23 | As a programmer who loves doing low-level optimisation, 24 | but also appreciates high-level functional programming, 25 | I was charmed by this new language. 26 | 27 | [rust]: http://rust-lang.org 28 | [reddit]: http://reddit.com/r/programming 29 | 30 | I followed the development for a while, 31 | but I never got around to actually writing some code -- until now. 32 | As an exercise, I decided to port [Luculentus][luculentus] to Rust. 33 | Luculentus is a proof of concept spectral path tracer that I wrote for a graphics programming course. 34 | It is written in C++. 35 | I expect that porting it will allow me to learn many aspects of Rust. 36 | You can follow the development of the port at [GitHub][robigo-luculenta]. 37 | 38 | I also plan on refeshing Luculentus a bit, to use more idiomatic modern C++. 39 | When I wrote the path tracer in 2012, there was only partial support for C++11, 40 | so Luculentus still uses a lot of raw pointers and non-default destructors. 41 | I hope to make a fair comparison of resource management in Rust and modern C++ down the line. 42 | 43 | [luculentus]: https://github.com/ruuda/luculentus 44 | [robigo-luculenta]: https://github.com/ruuda/robigo-luculenta 45 | -------------------------------------------------------------------------------- /posts/writing-a-path-tracer-in-rust-part-2-first-impressions.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Writing a path tracer in Rust, part 2: first impressions 3 | header: Writing a path tracer in Rust 4 | break: path tracer 5 | subheader: First impressions 6 | part: 2 7 | lang: en-GB 8 | date: 2014-08-13 9 | synopsis: After setting my first baby steps in the Rust programming language, these are my initial impressions. 10 | --- 11 | 12 | As a learning exercise, I am porting the [Luculentus][luculentus] spectral path tracer to [Rust][rust]. 13 | You can follow the port on [GitHub][robigo-luculenta]. 14 | After porting a few files, these are my first impressions of Rust. 15 | 16 | [rust]: http://rust-lang.org 17 | [luculentus]: https://github.com/ruuda/luculentus 18 | [robigo-luculenta]: https://github.com/ruuda/robigo-luculenta 19 | 20 | Ecosystem 21 | --------- 22 | Installing Rust and [Cargo][cargo] was easier than I expected. 23 | With the installer, it even works on Windows without having to go through all the MSYS hassle. 24 | The Windows version is 32-bit though. 25 | 26 | Cargo is awesome! 27 | It is similar to what [Cabal][cabal] is for Haskell. 28 | Setting up a project with Cargo is as easy as writing a four-line [toml][toml] file. 29 | Then you just do `cargo run`, and it compiles everything, and then runs the program. 30 | The compiler produces mostly helpful error messages, telling you not only what the problem is, but also how to fix it. 31 | Compiling and running is fast (at this point, at least). 32 | For the few source files I have, it takes 0.46 seconds to compile and run on Linux. 33 | That feels like compilation is _instant_. 34 | Windows is slower, at 1.16 seconds. 35 | 36 | One downside of Cargo is that it only looks for `Cargo.toml`, 37 | and I dislike having uppercase characters in my filenames. 38 | At least `make` accepts `makefile` as well. 39 | Apparently, there is [not going to be][issue45] support for `cargo.toml` either. 40 | 41 | [cargo]: http://crates.io 42 | [cabal]: http://www.haskell.org/cabal/ 43 | [toml]: https://github.com/toml-lang/toml 44 | [issue45]: https://github.com/rust-lang/cargo/issues/45 45 | 46 | Style 47 | ----- 48 | The official Rust style is to use [Egyptian brackets][egypt]. 49 | Though I prefer balanced brackets, 50 | it is a matter of taste. 51 | The official casing rules are Pascal casing for types, and lowercase with underscores (snake case) for most other things. 52 | Again, this is a matter of preference, but I do find it an odd combination. 53 | It leads to problems when a type is part of a function name, or when you name modules after a type. 54 | The standard library itself has `TreeMap` in `treemap.rs`, but `PriorityQueue` in `priority_queue.rs`. 55 | I chose to use snake case for my filenames. 56 | 57 | An other thing that surprised me, is that mathematical functions use method call syntax. 58 | That is, `x.sin()` instead of `sin(x)`. 59 | It felt awkward at first, but I got used to it very soon. 60 | It might even be better when multiple functions are nested, because the parentheses do not pile up. 61 | 62 | [egypt]: http://blog.codinghorror.com/new-programming-jargon/ 63 | 64 | Modules 65 | ------- 66 | Rust uses modules, which are like namespaces. 67 | The compiler compiles only one file, and it might look for other files when modules are declared. 68 | For example, I have a file `src/vector3.rs`, which will become the `vector3` module. 69 | In `main.rs`, you declare `mod vector3;`, and that will expand to `mod vector3 { content }`, with the contents of `vector3.rs`. 70 | This is very much like `#include` in C, and it surprised me at first. 71 | The `Vector3` type in `vector3.rs` is used in many other modules such as `ray`, so at first I thought I should also declare `mod vector3;` in `ray.rs`. 72 | However, that declares the module `::ray::vector3`. 73 | The proper thing to do, is to declare both the `vector3` and the `ray` module in `main.rs`, 74 | and then the module `::vector3` is available in `ray.rs`. 75 | If you keep in mind that module declarations work in this `#include` kind of way, it makes sense. 76 | 77 | Being used to the C# system, where all files are considered for name resolution, 78 | it does feel like a step backwards. 79 | I do not want to declare `vector3` in `main.rs`: it is a dependency of most other modules, but main does not use it directly. 80 | Changing things in `main.rs` changes the behaviour of `ray.rs`, 81 | even though main should depend on ray, 82 | not the other way around. 83 | Files interact in a [complex][complex] way. 84 | I might have missed something though, so please let me know if there is a better way. 85 | 86 | **Edit:** Thanks for the feedback, it changed my view. 87 | The `use mod::Type` syntax _is_ similar to `using` directives in C#. 88 | (Though in C# it is more common to use an entire namespace, not the individual types.) 89 | The `mod` declarations are more like a project file, they tell the compiler which files to consider. 90 | 91 | So far, translating C++ to Rust has been a pleasant experience. 92 | When things do not work as I expected, the IRC channel is very helpful. 93 | Next time I will discuss operator overloading with traits. 94 | 95 | Discuss this post on [Reddit][reddit]. 96 | Rust 0.12.0-pre-nightly was used in this post. 97 | 98 | [complex]: http://www.infoq.com/presentations/Simple-Made-Easy 99 | [reddit]: http://reddit.com/r/rust/ruudvanasseldonk.com/2014/08/13/writing-a-path-tracer-in-rust-part-2-first-impressions 100 | -------------------------------------------------------------------------------- /posts/writing-a-path-tracer-in-rust-part-3-operators.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Writing a path tracer in Rust, part 3: operators 3 | header: Writing a path tracer in Rust 4 | break: path tracer 5 | subheader: Operators 6 | part: 3 7 | lang: en-GB 8 | date: 2014-08-15 9 | synopsis: Operator overloading can do wonders to make vector math readable. How does Rust compare to C++ here? 10 | --- 11 | 12 | As a learning exercise, I am porting the [Luculentus][luculentus] spectral path tracer to [Rust][rust]. 13 | You can follow the port on [GitHub][robigo-luculenta]. 14 | This post will outline the vector and quaternion implementations, 15 | and I will highlight some of the differences between C++ and Rust. 16 | 17 | [rust]: http://rust-lang.org 18 | [luculentus]: https://github.com/ruuda/luculentus 19 | [robigo-luculenta]: https://github.com/ruuda/robigo-luculenta 20 | 21 | Vector3 22 | ------- 23 | The vector shows up in virtually every program that manipulates geometry. 24 | Luculentus has a fairly straightforward `Vector3`: 25 | 26 | ```cpp 27 | struct Vector3 28 | { 29 | float x, y, z; 30 | 31 | inline float MagnitudeSquared() const 32 | { 33 | return Dot(*this, *this); 34 | } 35 | }; 36 | ``` 37 | 38 | There are also `Magnitude` and `Normalise` methods that I omitted here. 39 | C++ allows initialization like this: 40 | 41 | ```cpp 42 | Vector3 v = { 1.0f, 0.0f, 0.0f }; 43 | ``` 44 | 45 | The vector in Rust is similar: 46 | 47 | ```rust 48 | pub struct Vector3 { 49 | pub x: f32, 50 | pub y: f32, 51 | pub z: f32 52 | } 53 | ``` 54 | 55 | Struct members are not public by default, and visibility can be specified for the struct as well. 56 | In Rust, there is a clear separation between code and data. 57 | Methods are defined in a separate `impl` block, outside of the struct: 58 | 59 | ```rust 60 | impl Vector3 { 61 | pub fn magnitude_squared(self) -> f32 { 62 | dot(self, self) 63 | } 64 | } 65 | ``` 66 | 67 | Again, there are more methods here that I omitted. 68 | Note that there is no explicit `return`: by omitting a semicolon, 69 | the last expression in a function determines the return value. 70 | Initialization in Rust can be done as follows: 71 | 72 | ```rust 73 | let v = Vector3 { x: 1.0, y: 1.0, z: 1.0 }; 74 | ``` 75 | 76 | Note that the numbers do not need an `f` suffix, even though they are single-precision floats. 77 | The type of a literal can depend on the context. 78 | 79 | Operator overloading 80 | -------------------- 81 | There are a few obvious operators to overload for `Vector3`: binary + and -, unary -, and binary * for scalar multiplication. 82 | One way to overload + in C++ is this: 83 | 84 | ```cpp 85 | inline Vector3 operator+(const Vector3 a, const Vector3 b) 86 | { 87 | Vector3 sum = { a.x + b.x, a.y + b.y, a.z + b.z }; 88 | return sum; 89 | } 90 | ``` 91 | 92 | In Rust, overloading + involves implementing the `Add` trait. 93 | Traits are like interfaces in C#. 94 | `Add` takes two generic parameters: the type of the right-hand side, and the type of the result. 95 | Both are `Vector3` in this case. 96 | 97 | ```rust 98 | impl Add for Vector3 { 99 | fn add(&self, other: &Vector3) -> Vector3 { 100 | Vector3 { 101 | x: self.x + other.x, 102 | y: self.y + other.y, 103 | z: self.z + other.z 104 | } 105 | } 106 | } 107 | ``` 108 | 109 | Overloading * for scalar multiplication in C++ is similar to addition: 110 | 111 | ```cpp 112 | inline Vector3 operator*(const Vector3 a, const float f) 113 | { 114 | Vector3 prod = { a.x * f, a.y * f, a.z * f }; 115 | return prod; 116 | } 117 | ``` 118 | 119 | In Rust, the `Mul` trait takes two type parameters as well: the type of the right-hand side, and the type of the result. 120 | 121 | ```rust 122 | impl Mul for Vector3 { 123 | fn mul(&self, f: &f32) -> Vector3 { 124 | Vector3 { 125 | x: self.x * *f, 126 | y: self.y * *f, 127 | z: self.z * *f 128 | } 129 | } 130 | } 131 | ``` 132 | 133 | This looks a bit awkward, because `f` must be dereferenced: 134 | the `Mul` trait dictates that that `mul` takes its arguments by reference. 135 | Note that `self` is automatically dereferenced in `self.x`, there is no `->` like in C++. 136 | 137 | Now we can wite things like `v * f` where `v` is a vector and `f` a scalar. 138 | Can we also implement `f * v`? 139 | In C++ it is straightforward, just switch the arguments. 140 | In Rust, I think it cannot be done at this point. 141 | Multiplication is a `mul` call on the left hand side, so we would have to implement `Mul` for `f32`. 142 | Unfortuntely, the compiler allows only one implementation of `Mul` for a type, regardless of the type parameters for `Mul`. 143 | Because regular multiplication of two `f32`s implements `Mul` already, we cannot implement it for `Vector3` any more. 144 | 145 | Quaternions 146 | ----------- 147 | Luculentus uses quaternions to represent rotations. 148 | Most of the `Quaternion` implementation is similar to that of `Vector3`, but with four components instead of three. 149 | (In fact, quaternions form a vector space, so they _are_ vectors.) 150 | The interesting thing is that quaternions support quaternion multiplication in addition to scalar multiplication. 151 | 152 | In C++, we can implement them both: 153 | 154 | ```cpp 155 | inline Quaternion operator*(const Quaternion q, const float f) 156 | { 157 | Quaternion prod = { q.x * f, q.y * f, q.z * f, q.w * f }; 158 | return prod; 159 | } 160 | 161 | inline Quaternion operator*(const Quaternion a, const Quaternion b) 162 | { 163 | Quaternion prod = 164 | { 165 | a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y, 166 | a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x, 167 | a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w, 168 | a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z 169 | }; 170 | return prod; 171 | } 172 | ``` 173 | 174 | As we saw before, we cannot implement `Mul` twice in Rust. 175 | Luckily, the IRC channel was very helpful, and there is a solution. 176 | The trick is to define a trait for “things that can be the right-hand side of multiplication with a quaternion”: 177 | 178 | ```rust 179 | trait MulQuaternion { 180 | fn mul(&self, lhs: &Quaternion) -> Quaternion; 181 | } 182 | ``` 183 | 184 | Then we can implement `Mul` where the right-hand side must implement `MulQuaternion`: 185 | 186 | ```rust 187 | impl Mul for Quaternion { 188 | fn mul(&self, other: &T) -> Quaternion { 189 | other.mul(self) 190 | } 191 | } 192 | ``` 193 | 194 | Finally, we can implement `MulQuaternion` for `f32` as well as `Quaternion` itself: 195 | 196 | ```rust 197 | impl MulQuaternion for f32 { 198 | fn mul(&self, lhs: &Quaternion) -> Quaternion { 199 | Quaternion { 200 | x: lhs.x * *self, 201 | y: lhs.y * *self, 202 | z: lhs.z * *self, 203 | w: lhs.w * *self 204 | } 205 | } 206 | } 207 | 208 | impl MulQuaternion for Quaternion { 209 | fn mul(&self, lhs: &Quaternion) -> Quaternion { 210 | Quaternion { 211 | x: lhs.w * self.x + lhs.x * self.w + lhs.y * self.z - lhs.z * self.y, 212 | y: lhs.w * self.y - lhs.x * self.z + lhs.y * self.w + lhs.z * self.x, 213 | z: lhs.w * self.z + lhs.x * self.y - lhs.y * self.x + lhs.z * self.w, 214 | w: lhs.w * self.w - lhs.x * self.x - lhs.y * self.y - lhs.z * self.z 215 | } 216 | } 217 | } 218 | ``` 219 | 220 | It feels a bit weird, because the second argument is the left-hand side of the multiplication. 221 | Like with `Vector3`, I think it is impossible to implement scalar multiplication with the scalar on the left. 222 | Please let me know if I am wrong! 223 | There is [an RFC][rfc] for multidispatch in traits. 224 | If it gets accepted, it might allow multiple implementations of `Add` and `Mul` for the same type. 225 | The correct one would be selected based on the types of the operands (or method arguments in general). 226 | That would certainly simplify the quaternion code, and it would allow scalar multiplication with the scalar on the left. 227 | 228 | [rfc]: https://github.com/rust-lang/rfcs/pull/195 229 | 230 | **Edit:** Rust 0.13 does have multidispatch now, 231 | and the code has been simplified accordingly. 232 | 233 | Next time I will discuss more of the type system, 234 | and there will finally be rays! 235 | I will also discuss more of the internals of the path tracer. 236 | 237 | Discuss this post on [Reddit][reddit]. 238 | Rust 0.12.0-pre-nightly was used in this post. 239 | 240 | [reddit]: http://reddit.com/r/rust/ruudvanasseldonk.com/2014/08/15/writing-a-path-tracer-in-rust-part-3-operators 241 | -------------------------------------------------------------------------------- /posts/writing-a-path-tracer-in-rust-part-4-tracing-rays.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Writing a path tracer in Rust, part 4: tracing rays 3 | header: Writing a path tracer in Rust 4 | break: path tracer 5 | subheader: Tracing rays 6 | part: 4 7 | date: 2014-08-19 8 | lang: en-GB 9 | synopsis: Tracing rays is at the core of a path tracer. Implementing the algorithm in Rust is a great way to get a feel for the language. 10 | --- 11 | 12 | As a learning exercise, I am porting the [Luculentus][luculentus] spectral path tracer to [Rust][rust]. 13 | You can follow the port on [GitHub][robigo-luculenta]. 14 | This post will outline scene intersection and materials, 15 | and I will highlight some of the differences between C++ and Rust. 16 | 17 | [rust]: http://rust-lang.org 18 | [luculentus]: https://github.com/ruuda/luculentus 19 | [robigo-luculenta]: https://github.com/ruuda/robigo-luculenta 20 | 21 | Rays 22 | ---- 23 | Last time I promised there would be rays, so here they are: 24 | 25 | ```cpp 26 | struct Ray 27 | { 28 | Vector3 origin; 29 | Vector3 direction; 30 | float wavelength; 31 | float probability; 32 | }; 33 | ``` 34 | 35 | Because Luculentus is a spectral path tracer, every ray has an associated wavelength. 36 | Interaction with surfaces depends on this wavelength. 37 | Every ray also has an associated probability, which acts similar to colour components in a regular path tracer. 38 | In Rust, the ray looks like this: 39 | 40 | ```rust 41 | pub struct Ray { 42 | pub origin: Vector3, 43 | pub direction: Vector3, 44 | pub wavelength: f32, 45 | pub probability: f32 46 | } 47 | ``` 48 | 49 | Now we need something to intersect a ray with: a surface. 50 | In C++, the intersection and surface are defined as follows: 51 | 52 | ```cpp 53 | struct Intersection 54 | { 55 | Vector3 position; 56 | Vector3 normal; 57 | Vector3 tangent; 58 | float distance; 59 | }; 60 | 61 | struct Surface 62 | { 63 | virtual bool Intersect(const Ray ray, 64 | Intersection& intersection) const = 0; 65 | }; 66 | ``` 67 | An `Intersection` contains details about the surface at the intersection, 68 | and the distance along the ray. 69 | The distance is later used to pick the closest intersection. 70 | A surface is just an abstract base class with an `Intersect` method. 71 | The method takes an intersection by reference. 72 | If the surface was intersected, it returns `true` and the intersection is filled with the details. 73 | If the ray did not intersect the surface, it returns `false`. 74 | An other approach would be to return an `Intersection*`, and return null when there is no intersection. 75 | This would involve a heap allocation, so I opted for the first approach. 76 | 77 | Rust has a cleaner way to handle optional values: the `Option` type. 78 | The intersection and surface in Rust are defined like this: 79 | 80 | ```rust 81 | pub struct Intersection { 82 | pub position: Vector3, 83 | pub normal: Vector3, 84 | pub tangent: Vector3, 85 | pub distance: f32 86 | } 87 | 88 | pub trait Surface { 89 | fn intersect(&self, ray: &Ray) -> Option; 90 | } 91 | ``` 92 | 93 | Whereas in C++, surface classes derive from `Surface`, in Rust they implement a trait. 94 | The `intersect` method returns some intersection if there was one, or `None` if nothing was intersected. 95 | I find this to be a more natural approach than an out argument. 96 | Note that even though this is like returning a pointer that might be null in C++, 97 | there is no heap allocation involved here. 98 | 99 | Materials 100 | --------- 101 | Now we can intersect surfaces, but there is an other part to path tracing. 102 | When a surface is intersected, the material at that point determines how the light path continues. 103 | In C++, all non-emissive materials derive from `Material`: 104 | 105 | ```cpp 106 | struct Material 107 | { 108 | virtual Ray GetNewRay(const Ray incomingRay, 109 | const Intersection intersection, 110 | MonteCarloUnit& monteCarloUnit) const = 0; 111 | }; 112 | ``` 113 | 114 | The material takes the incoming ray and intersection details, 115 | and produces a new ray. 116 | This method need not be deterministic, so a Monte Carlo unit is provided as well, 117 | which is a wrapper around a random number generator. 118 | Every thread has its own Monte Carlo unit, so there is no race for random numbers. 119 | 120 | In Rust, `Material` is a trait: 121 | 122 | ```rust 123 | pub trait Material { 124 | fn get_new_ray(&self, 125 | incoming_ray: &Ray, 126 | intersection: &Intersection) 127 | -> Ray; 128 | } 129 | ``` 130 | 131 | Rust has a task-local random number generator, 132 | so there is no need to provide one explicitly: random number generation cannot race. 133 | 134 | Besides a regular reflective material, Luculentus also has an `EmissiveMaterial` for light sources. 135 | `EmissiveMaterial` has one method that returns the light intensity for a given wavelength. 136 | The Rust trait is similar to the abstract class in C++. 137 | This approach is great when the light source has a broad spectrum (like the sun or a light bulb), 138 | but it does not work for spectra with only a few spectral lines (like a natrium lamp). 139 | Because wavelengths are chosen at random, the probability of hitting a spectral line is too low. 140 | This could be compensated for by not choosing wavelengths at random, 141 | but Luculentus is not that advanced. 142 | 143 | Objects 144 | ------- 145 | The scene in Luculentus consists of _objects_. 146 | Objects have some geometry described by a `Surface`, 147 | and they can either be emissive (for light sources) or reflective. 148 | In C++, this is done as follows: 149 | 150 | ```cpp 151 | struct Object 152 | { 153 | std::shared_ptr surface; 154 | std::shared_ptr material; 155 | std::shared_ptr emissiveMaterial; 156 | }; 157 | ``` 158 | 159 | The `surface` pointer must never be null, 160 | and either the material or emissive material must be non-null. 161 | It works, but the compiler does not prevent you from creating an invalid object 162 | that contains no material, or both a reflective and emissive material. 163 | It could be improved a bit by using a tagged union, but for this simple case, two pointers suffice. 164 | In Rust, valid objects can be enforced statically: 165 | 166 | ```rust 167 | pub enum MaterialBox { 168 | Reflective(Box), 169 | Emissive(Box) 170 | } 171 | 172 | pub struct Object { 173 | pub surface: Box, 174 | pub material: MaterialBox 175 | } 176 | ``` 177 | 178 | Enums in Rust are more than glorified integers: they enable [sum types][sumtype]. 179 | There is no way to assign both a reflective and emissive material to an object now, 180 | and because Rust does not have null pointers, 181 | there is also no way to assign _no_ material to an object. 182 | Much better than the C++ version! 183 | 184 | The `Box` is like a `unique_ptr` in C++. 185 | Note that the types inside `Box` are the traits we defined above, not structs. 186 | This is the way to do runtime polymorphism in Rust. 187 | The value in the `Box` could have any type, provided that it implements the right trait. 188 | 189 | [sumtype]: https://en.wikipedia.org/wiki/Algebraic_data_type 190 | 191 | We now have everything to build and intersect a scene. 192 | Luculentus is a simple proof-of-concept path tracer, 193 | so there is no data structure for fast scene intersection. 194 | The scene is just a vector of objects, and to intersect it, 195 | we intersect all objects, and return the closest intersection. 196 | 197 | Putting it together 198 | ------------------- 199 | Given a ray, we would like to simulate a light path (from the camera backwards), 200 | until a light source is hit. 201 | Then we can compute the intensity of light along this path. 202 | 203 | ```cpp 204 | Ray ray = camera.GetRay(monteCarloUnit); 205 | float intensity = 1.0f; 206 | do 207 | { 208 | Intersection intersection; 209 | const Object* object = scene->Intersect(ray, intersection); 210 | 211 | if (!object) return 0.0f; 212 | 213 | if (!object->material) 214 | { 215 | // If material is null, emissiveMaterial is not null. 216 | return intensity * 217 | object->emissiveMaterial->GetIntensity(ray.wavelength); 218 | } 219 | 220 | ray = object->material->GetNewRay(ray, intersection, monteCarloUnit); 221 | intensity *= ray.probability; 222 | } 223 | while (...) 224 | ``` 225 | 226 | We intersect a ray with the scene. 227 | If nothing was hit, the light intensity is zero -- a black background. 228 | If an object was intersected, and its `material` pointer is null, 229 | its `emissiveMaterial` is not null by assumption, so the object is a light source. 230 | The final intensity is the intensity of the light source reduced by the effects of previous bounces. 231 | If the `material` pointer was not null, 232 | we ask the material to generate a ray that continues the path. 233 | The loop continues with a probability that decreases with every intersection. 234 | For simplicity, I omitted the details in the code. 235 | Paths with a low intensity also have a higher chance of being terminated. 236 | If the loop terminates, the intensity is just zero. 237 | 238 | The Rust version uses pattern matching instead of null pointers: 239 | 240 | ```rust 241 | let mut ray = camera.get_ray(); 242 | let mut intensity = 1.0f32; 243 | loop { 244 | match scene.intersect(&ray) { 245 | None => return 0.0, 246 | Some((intersection, object)) => { 247 | match object.material { 248 | Emissive(ref mat) => { 249 | return intensity * mat.get_intensity(ray.wavelength); 250 | }, 251 | Reflective(ref mat) => { 252 | ray = mat.get_new_ray(&ray, &intersection); 253 | intensity = intensity * ray.probability; 254 | } 255 | } 256 | } 257 | } 258 | 259 | if ... { break; } 260 | } 261 | ``` 262 | 263 | I find the C++ version more aesthetically pleasing and readable. 264 | The `Emissive` and `Reflective` enum variants contain a `Box` with the material. 265 | If we were to match on that, it would move the box into the match variable. 266 | Here we do not want to take ownership of the material, 267 | so by matching with `ref`, the `mat` variables will borrow the material instead. 268 | 269 | For the cases in this post, the types in Rust and C++ are very similar. 270 | However, Rust has a more advanced type system, with several benefits: 271 | it prevents you from constructing invalid objects, 272 | and it forces you to consider every case. 273 | Next time I will discuss how ray intensities are converted into an image. 274 | 275 | Discuss this post on [Reddit][reddit]. 276 | Rust 0.12.0-pre-nightly was used in this post. 277 | 278 | [reddit]: http://reddit.com/r/rust/ruudvanasseldonk.com/2014/08/19/writing-a-path-tracer-in-rust-part-4-tracing-rays 279 | -------------------------------------------------------------------------------- /posts/writing-a-path-tracer-in-rust-part-7-conclusion.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Writing a path tracer in Rust, part 7: conclusion 3 | header: Writing a path tracer in Rust 4 | break: path tracer 5 | subheader: Conclusion 6 | part: 7 7 | date: 2014-10-20 8 | lang: en-GB 9 | synopsis: After porting a reasonably-sized application to Rust, I still like the language a lot. C++ has a worthy opponent here. 10 | --- 11 | 12 | As a learning exercise, 13 | I have ported the [Luculentus][luculentus] spectral path tracer to [Rust][rust]. 14 | The result is available on [GitHub][robigo-luculenta]. 15 | In the process, I have also refreshed Luculentus a bit, updating it to modern C++. 16 | You can read about the details in the previous posts. 17 | In this post, I want to outline the process, and compare the final versions. 18 | 19 | [rust]: http://rust-lang.org 20 | [luculentus]: https://github.com/ruuda/luculentus 21 | [robigo-luculenta]: https://github.com/ruuda/robigo-luculenta 22 | 23 | Eyecandy 24 | -------- 25 | First of all, the output of the path tracer! 26 | The scene is hard-coded, and it looks like this: 27 | 28 | ![the output of Robigo Luculenta](/images/robigo-luculenta.jpg) 29 | 30 | If you want to play with it, 31 | take a look at `set_up_scene` in [app.rs][apprs]. 32 | 33 | [apprs]: https://github.com/ruuda/robigo-luculenta/blob/master/src/app.rs 34 | 35 | Getting started with Rust 36 | ------------------------- 37 | You can install the [Rust compiler][install] 38 | and [Cargo][cargo] within minutes nowadays, 39 | even on Windows. 40 | It was much easier to get this working than e.g. [Scala][scala] with [sbt][sbt]. 41 | 42 | [install]: http://www.rust-lang.org/install.html 43 | [winwikipage]: https://github.com/rust-lang/rust/wiki/Using-Rust-on-Windows 44 | [cargo]: http://crates.io/ 45 | [scala]: http://scala-lang.org/ 46 | [sbt]: http://www.scala-sbt.org/ 47 | 48 | I have found the Rust community to be very nice. 49 | When I did not know what to do, the [IRC channel][irc] helped me out, 50 | and the [subreddit][r/rust] was very useful as well. 51 | The core team is around there as well, 52 | so an answer from the experts is not uncommon. 53 | 54 | [irc]: irc://irc.mozilla.org/rust 55 | [r/rust]: http://www.reddit.com/r/rust 56 | 57 | At this point, Rust is a fast moving target. 58 | During the porting process, a handful of functions has been deprecated, 59 | functions have been renamed, 60 | and even bits of syntax have changed. 61 | Some people describe Rust today as a different language than it was a year ago, 62 | but I have not been using it long enough to experience that. 63 | Ultimately all changes should make the language better and more consistent, 64 | and I am confident that Rust 1.0 will be a great language. 65 | 66 | Ownership 67 | --------- 68 | If I had to describe Rust in one word, it would be _ownership_. 69 | For me, this is the one thing that sets Rust apart from other languages. 70 | In most languages, ownership is implicit, 71 | and this leads to several kinds of errors. 72 | When a function returns a pointer in C, who is responsible for freeing it? 73 | Can you answer that question without consulting the documentation? 74 | And even if you know the answer, it is still possible to forget a free, 75 | or accidentally free twice. 76 | 77 | This problem is not specific to pointers though, 78 | it is a problem with resources in general. 79 | It may seem that garbage collection is a good solution, 80 | but it only deals with memory. 81 | Then you need an other way to free non-memory resources like a file handle, 82 | and all problems reappear. 83 | For example, the garbage collector in C# prevents use after free, 84 | but there is nothing that prevents use after _dispose_. 85 | Is an `ObjectDisposedException` that much better than an access violation? 86 | Due to explicit lifetimes and ownership, 87 | Rust does not have these kinds of errors. 88 | 89 | A static type system prevents runtime type errors 90 | that can occur in a dynamically typed language, 91 | but it has to be more strict at compile time. 92 | Rust’s ownership prevents runtime errors that can occur 93 | due to incorrect resource management, 94 | but it has to be even more strict at compile time. 95 | The rigorous approach to ownership makes it harder to write valid code, 96 | but if the compiler refuses to compile your code, 97 | there often is a real problem in it, 98 | which would go unnoticed in languages where ownership is implicit. 99 | Rust forces you to consider ownership, 100 | and this guides you towards a better design. 101 | 102 | Updating Luculentus 103 | ------------------- 104 | The benefits of ownership are not specific to Rust. 105 | It is perfectly possible to write similar code in modern C++, 106 | which is arguably a very different language than pre-2011 C++. 107 | When I wrote Luculentus, C++11 was only partially supported. 108 | There were lots of raw pointers that are nowadays not necessary. 109 | I have replaced most raw pointers in Luculentus with `shared_ptr` or `unique_ptr`, 110 | and arrays with vectors. 111 | As a consequence, **all** manual destructors are now gone. 112 | (There were six previously.) 113 | Before, there were eleven delete statements. 114 | Now there are zero. 115 | All memory management has become automatic. 116 | This not only makes the code more concise, 117 | it also eliminates room for errors. 118 | 119 | Porting the path tracer to Rust improved its design. 120 | If your resource management is wrong, it is invalid in Rust. 121 | In C++ you can get away with e.g. taking the address of an element of a vector, 122 | and when the vector goes out of scope, the pointer will be invalid. 123 | The code is valid C++ though. 124 | Rust does not allow shortcuts like that, 125 | and for me it has opened my eyes to an area that I was not fully aware of before. 126 | Even when working in other languages, 127 | if a construct would be illegal in Rust, 128 | there probably is a better way. 129 | 130 | Still, the update demonstrates that it _is_ possible to write safe code in modern C++. 131 | You _do_ get safe, automatic memory management with virtually no overhead. 132 | The only caveat is that you must choose to leverage it. 133 | You could use a `unique_ptr`, but you could just as well use a raw pointer. 134 | All the dangerous tools of the ‘old’ C++ are still there, 135 | and you can mix them with modern C++ if you like. 136 | Of course there is value in having old code compile (Bjarne calls it a [feature][feature]), 137 | but I would prefer to not implicitly mix two quite different paradigms, 138 | or keep all the past design mistakes around. 139 | It takes some time to unlearn using `new` and `delete`, 140 | and even then, old APIs will be with us for a long time. 141 | 142 | [feature]: http://channel9.msdn.com/Events/GoingNative/2013/Opening-Keynote-Bjarne-Stroustrup 143 | 144 | A fresh start 145 | ------------- 146 | A nice thing about Rust is that it can start from scratch, 147 | and learn from the mistakes of earlier languages. 148 | C++11 is a lot better than its predecessor, 149 | but it only _adds_ features, 150 | and every new feature cannot break old code. 151 | One point where this shows is syntax. 152 | In Rust, types go after the name, and a return type comes after the argument list, 153 | which is the sensible thing to do. 154 | Rust’s lambda syntax is more concise, and there is less repetition. 155 | I still cannot get used to the Egyptian brackets though. 156 | They look wrong to me. 157 | 158 | Another area where I think Rust made the right choice, is mutability. 159 | In Rust, everything is immutable by default, 160 | whereas in C++ everything is mutable by default. 161 | The Luculentus codebase has 535 occurences of `const` at the moment of writing. 162 | Robigo Luculenta has only 97 occurences of `mut`. 163 | Of course there is more duplication in C++, 164 | but this still suggests that immutable is a more sensible default. 165 | Also, the Rust compiler warns about variables that need not be mutable, 166 | which is nice. 167 | 168 | Although syntax is to some extent a matter of preference, 169 | there are quantitative measures as well. 170 | If I compare the number of non-whitespace source characters, 171 | the C++ version has roughly 109 thousand characters 172 | -- excluding the files that I did not port 173 | -- whereas the Rust version has roughly 74 thousand characters, 174 | about two thirds the size of the C++ version. 175 | 176 | C++ is notorious for its cryptic error messages 177 | when a template expansion does not work out. 178 | Rust’s errors are mostly comprehensible, 179 | but some can be intimidating as well: 180 | 181 | error: binary operation `/` cannot be applied to type `core::iter::Map<'_,f32,f32,core::iter::Map<'_,&[f32],f32,core::slice::Chunks<'_,f32>>>` 182 | 183 | Performance 184 | ----------- 185 | I added basic performance counters to Luculentus and Robigo Luculenta. 186 | It counts the number of trace tasks completed per second. 187 | These are the results: 188 | 189 | Compiler Platform Performance 190 | --------------------- -------------- ----------- 191 | GCC 4.9.1* Arch Linux x64 0.35 ± 0.04 192 | GCC 4.9.1 Arch Linux x64 0.33 ± 0.06 193 | rustc 0.12 2014-09-25 Arch Linux x64 0.32 ± 0.01 194 | clang 3.5.0 Arch Linux x64 0.30 ± 0.05 195 | MSVC 110 Windows 7 x64 0.23 ± 0.03 196 | MSVC 110* Windows 7 x64 0.23 ± 0.02 197 | rustc 0.12 2014-09-23 Windows 7 x64 0.23 ± 0.01 198 | 199 | Optimisation levels were set as high as possible everywhere. 200 | The compilers with asterisk used profile-guided optimisation. 201 | The only conclusion I can draw from this, 202 | is that you should probably not use Windows if you want performance 203 | from CPU-bound applications. 204 | 205 | In the second post in this series, 206 | I noted that rustc compiles extremely fast, 207 | but there was very little code at that point. 208 | After the port, these are the compile times in seconds: 209 | 210 | Compiler Time 211 | --------------------- ------------ 212 | rustc 0.12 2014-09-26 7.31 ± 0.05 213 | clang 3.5.0 13.39 ± 0.03 214 | GCC 4.9.1 17.3 ± 0.5 215 | MSVC 110 20.4 ± 0.3 216 | 217 | No instant compilation any more, but still much better than C++. 218 | 219 | Conclusion 220 | ---------- 221 | Learning Rust was a fun experience. 222 | I like the language, and the port lead to a few insights 223 | that could improve the original code as well. 224 | Ownership is often implicit in other languages, 225 | which means it is prone to human error. 226 | Rust makes it explicit, eliminating these errors. 227 | Safety is not an opt-in, it is the default. 228 | This puts Rust definitely more on the ‘stability’ side of the spectrum 229 | than the ‘rapid development’ side. 230 | I have written not nearly enough code in Rust to make a fair judgement, 231 | but so far, Rust’s advantages outweigh the minor annoyances. 232 | If I could choose between C++ and Rust for my next project, 233 | I would choose Rust. 234 | 235 | Discuss this post on [Reddit][reddit]. 236 | 237 | [reddit]: http://reddit.com/r/rust/ruudvanasseldonk.com/2014/10/20/writing-a-path-tracer-in-rust-part-7-conclusion 238 | -------------------------------------------------------------------------------- /posts/yaose-is-now-free-software.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Yaose is now free software 3 | break: now free 4 | date: 2011-11-14 5 | lang: en-GB 6 | synopsis: I made my editor for Ogre script files available as free software. 7 | run-in: After a long time 8 | --- 9 | 10 | After a long time in which I have had little time to work on Yaose, 11 | I decided that it would be better to make Yaose free software. 12 | This way, users do not have to wait for me. 13 | 14 | Almost a year after previewing an alpha version, not much had changed. 15 | This is a shame, because I think Yaose has great potential. 16 | Therefore, the source code of Yaose is now 17 | [freely available](http://veniogames.com/downloads/yaose/free-software). 18 | 19 | The source code for Yaose is, however, slightly messy, 20 | and not organised very well. 21 | At first, I wanted to sort this out before releasing the source to the public. 22 | I realised that this would take a lot of time, 23 | and that it would take even longer to get a new version of Yaose out. 24 | I could also spend that time on developing useful features. 25 | Therefore, I decided to release the code at this point, maybe slightly messy. 26 | 27 | Get it at Gitorious: 28 | 29 | $ git clone git://gitorious.org/yaose/yaose.git 30 | 31 | -------------------------------------------------------------------------------- /posts/zero-cost-abstractions.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Zero-cost abstractions 3 | date: 2016-11-30 4 | minutes: 6 5 | lang: en-GB 6 | synopsis: Rust claims to enable abstraction without overhead. How does that claim hold up in practice? 7 | run-in: If you read my blog 8 | teaser: neither-necessary-nor-sufficient 9 | --- 10 | 11 | If you read my blog, 12 | you can probably guess that I like Rust. 13 | I like Rust for a variety of reasons. 14 | Its [error model][error-model], for example. 15 | But today I want to highlight a different reason: 16 | zero-cost abstractions. 17 | 18 | Recently I had the opportunity 19 | to do some optimisation work on [Claxon][claxon], 20 | my FLAC decoder. 21 | In the process I found a piece of code 22 | which in my opinion demonstrates the power of Rust very well, 23 | and I would like to share that with you here: 24 | 25 | ```rust 26 | for i in 12..buffer.len() { 27 | let prediction = coefficients.iter() 28 | .zip(&buffer[i - 12..i]) 29 | .map(|(&c, &s)| c * s as i64) 30 | .sum::() >> qlp_shift; 31 | let delta = buffer[i]; 32 | buffer[i] = prediction as i32 + delta; 33 | } 34 | ``` 35 | 36 | For context, the following variables are in scope: 37 | 38 | ```rust 39 | buffer: &mut [i32]; // A mutable array slice. 40 | coefficients: [i64; 12]; // A fixed-size array of 12 elements. 41 | qlp_shift: i16; 42 | ``` 43 | 44 | The snippet is part of a function that restores sample values from residues. 45 | This is something that happens a lot during decoding, 46 | and this particular loop makes up roughly 20% of the total decoding time. 47 | It had better be efficient. 48 | 49 | [error-model]: /2015/06/17/exceptional-results-error-handling-in-csharp-and-rust 50 | [claxon]: https://github.com/ruuda/claxon 51 | 52 | Ingredients 53 | ----------- 54 | 55 | The snippet computes a fixed-point arithmetic inner product 56 | between the coefficients and a window that slides over the buffer. 57 | This value is the prediction for a sample. 58 | After adding the residue to the prediction, the window is advanced. 59 | An inner product can be neatly expressed by zipping the coefficients with the window, 60 | mapping multiplication over the pairs, 61 | and then taking the sum. 62 | I would call this an abstraction: 63 | it moves the emphasis away from how the value is computed, 64 | focusing on a declarative specification instead. 65 | 66 | The snippet is short and clear -- in my opinion -- 67 | but there is quite a bit going on behind the scenes. 68 | Let’s break it down. 69 | 70 | * `12..buffer.len()` constructs a `Range` structure with an upper and lower bound. 71 | Iterating over it with a for loop implicitly creates an iterator, 72 | however in the case of `Range` that iterator is the structure itself. 73 | * `coefficients().iter()` constructs a slice iterator, 74 | and the call to `zip` implicitly constructs an iterator for the `buffer` slice as well. 75 | * `zip` and `map` both wrap their input iterators in a new iterator structure. 76 | * A closure is being passed to `map`. 77 | The closure does not capture anything from its environment in this case. 78 | * `sum` repeatedly calls `next()` on its input iterator, 79 | pattern matches on the result, 80 | and adds up the values until the iterator is exhausted. 81 | * Indexing into and slicing `buffer` will panic if an index is out of bounds, 82 | as Rust does not allow reading past the end of an array. 83 | 84 | It appears that these high-level constructs come at a price. 85 | Many intermediate structures are created 86 | which would not be present in a hand-written inner product. 87 | Fortunately these structures are not allocated on the heap, 88 | as they would likely be in a language like Java or Python. 89 | Iterators also introduce extra control flow: 90 | `zip` will terminate after one of its inner iterators is exhausted, 91 | so in principle it has to branch twice on every iteration. 92 | And of course iterating itself involves a call to `next()` on every iteration. 93 | Are we trading performance for convenience here? 94 | 95 | Generated code 96 | -------------- 97 | 98 | The only proper way to reason about the cost of these abstractions 99 | is to inspect the generated machine code. 100 | Claxon comes with a `decode` example program, 101 | which I compiled in release mode with Rustc 1.13 stable. 102 | Let’s take a look at the result: 103 | 104 | ```asm 105 | 10c00: 106 | movslq %r14d,%r11 107 | movslq -0x2c(%r8,%rdi,4),%rsi 108 | imul %r10,%rsi 109 | movslq -0x30(%r8,%rdi,4),%r14 110 | imul %rbp,%r14 111 | add %rsi,%r14 112 | movslq -0x28(%r8,%rdi,4),%rsi 113 | imul %rdx,%rsi 114 | add %rsi,%r14 115 | movslq -0x24(%r8,%rdi,4),%rsi 116 | imul %rax,%rsi 117 | add %rsi,%r14 118 | movslq -0x20(%r8,%rdi,4),%rsi 119 | imul %rbx,%rsi 120 | add %rsi,%r14 121 | movslq -0x1c(%r8,%rdi,4),%rsi 122 | imul %r15,%rsi 123 | add %rsi,%r14 124 | movslq -0x18(%r8,%rdi,4),%rsi 125 | imul %r13,%rsi 126 | add %rsi,%r14 127 | movslq -0x14(%r8,%rdi,4),%rsi 128 | imul %r12,%rsi 129 | add %rsi,%r14 130 | movslq -0x10(%r8,%rdi,4),%rsi 131 | imul 0x8(%rsp),%rsi 132 | add %rsi,%r14 133 | movslq -0xc(%r8,%rdi,4),%rsi 134 | imul 0x18(%rsp),%rsi 135 | add %rsi,%r14 136 | movslq -0x8(%r8,%rdi,4),%rsi 137 | imul 0x20(%rsp),%rsi 138 | add %rsi,%r14 139 | imul 0x10(%rsp),%r11 140 | add %r11,%r14 141 | sar %cl,%r14 142 | add (%r8,%rdi,4),%r14d 143 | mov %r14d,(%r8,%rdi,4) 144 | inc %rdi 145 | cmp %r9,%rdi 146 | jb 10c00 147 | ``` 148 | 149 | All overhead is gone **completely**. 150 | What happened here? 151 | First of all, no loop is used to compute the inner product. 152 | The input slices have a fixed size of 12 elements, 153 | and despite the use of iterators, 154 | the compiler was able to unroll everything here. 155 | The element-wise computations are efficient too. 156 | There are 12 `movslq`s which load a value from the buffer and simultaneously widen it. 157 | There are 12 multiplications and 12 additions, 158 | 11 for the inner product, 159 | and one to add the delta 160 | after arithmetic shifting (`sar`) the sum right. 161 | Note that the coefficients are not even loaded inside the loop, 162 | they are kept in registers at all times. 163 | After the sample has been computed, 164 | it is stored simply with a `mov`. 165 | All bounds checks have been elided. 166 | The final three instructions handle control flow of the loop. 167 | Not a single instruction is redundant here, 168 | and I could not have written this better myself. 169 | 170 | I don’t want to end this post without at least touching briefly upon vectorisation. 171 | It might look like the compiler missed an opportunity for vectorisation here, 172 | but I do not think that this is the case. 173 | For various reasons 174 | the above snippet is not as obvious to vectorise as it might seem at first sight. 175 | But in any case, 176 | a missed opportunity for vectorisation is just that: a missed opportunity. 177 | It is not abstraction overhead. 178 | 179 | Conclusion 180 | ---------- 181 | 182 | In this post I’ve shown a small snippet of code 183 | that uses high-level constructs such as closures and iterator combinators, 184 | yet the code compiles down to the same instructions that a hand-written C program would compile to. 185 | Rust lives up to its promise: 186 | abstractions are truly zero-cost. 187 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Blog 2 | ==== 3 | 4 | This is the source code for [my personal site][ruudva]. It is a static site 5 | generated by a homemade generator written in Haskell. 6 | 7 | The generator includes a tiny templating engine, an html and css minifier, and 8 | an aggressive font subsetter. One of my objectives was to cut all the crap 9 | (which almost by definition includes javascript) without compromising on 10 | design. An average page of my site weighs less than jQuery alone (which 11 | describes itself as “lightweight footprint”). That includes webfonts. 12 | 13 | This is version three of my blog. Previously I used [Hakyll][hakyll] (available 14 | in the `archived-hakyll` branch), and before that I used [Jekyll][jekyll]. 15 | 16 | [ruudva]: https://ruudvanasseldonk.com 17 | [hakyll]: http://jaspervdj.be/hakyll/ 18 | [jekyll]: http://jekyllrb.com/ 19 | 20 | License 21 | ------- 22 | The source code for this site is licensed under version 3 of the the 23 | [GNU General Public Licence][gplv3]. See the `licence` file. The content of the 24 | posts is licensed under the [Creative Commons BY SA][cc] licence. For the font 25 | license details, see the readme in the fonts directory. 26 | 27 | [gplv3]: https://gnu.org/licenses/gpl.html 28 | [cc]: https://creativecommons.org/licenses/by-sa/3.0/ 29 | 30 | Compiling 31 | --------- 32 | 33 | All dependencies are available in a [Nix][nix] ≥ 2.14 development environment 34 | that you can enter with 35 | 36 | $ nix develop --command $SHELL 37 | 38 | This will bring a `python3` on the path with the right requirements for font 39 | subsetting, as well as the blog generator itself, and tools for compressing 40 | images. 41 | 42 | The generator gets built as part of the development environment, but you can 43 | also compile it manually with GHC if you like. Then build the site (requires 44 | fonts to be present): 45 | 46 | $ ghc -o blog src/*.hs # Optional 47 | $ blog 48 | 49 | [nix]: https://nixos.org/nix/ 50 | -------------------------------------------------------------------------------- /src/Image.hs: -------------------------------------------------------------------------------- 1 | -- Copyright 2016 Ruud van Asseldonk 2 | -- 3 | -- This program is free software: you can redistribute it and/or modify 4 | -- it under the terms of the GNU General Public License version 3. See 5 | -- the licence file in the root of the repository. 6 | 7 | module Image (processImages) where 8 | 9 | import Codec.Picture (DynamicImage(..), imageWidth, imageHeight, readImage) 10 | import Codec.Picture.Types (dynamicMap) 11 | import Data.Foldable (foldrM) 12 | import Data.List (find, isSuffixOf, stripPrefix) 13 | import Data.Maybe (fromJust) 14 | import System.FilePath ((), takeFileName) 15 | import qualified Text.HTML.TagSoup as S 16 | 17 | import qualified Html 18 | 19 | type Attributes = [(String, String)] 20 | 21 | -- Returns the value of the "src" attribute. 22 | getSrc :: Attributes -> String 23 | getSrc attrs = case find ((== "src") . fst) attrs of 24 | Just (_src, value) -> value 25 | Nothing -> error $ "img tag without src attribute" 26 | 27 | makeDimensions :: DynamicImage -> Attributes 28 | makeDimensions img = [ ("width", show $ dynamicMap imageWidth img) 29 | , ("height", show $ dynamicMap imageHeight img) ] 30 | 31 | -- Given the attributes of an tag, which must include the src attribute, 32 | -- adds width and height attributes. 33 | addDimensions :: FilePath -> Attributes -> IO Attributes 34 | addDimensions imgDir attrs = fmap (attrs ++) dimensions 35 | where 36 | src = getSrc attrs 37 | imgPath = imgDir (takeFileName src) 38 | dimensions = 39 | if ".svg" `isSuffixOf` src 40 | -- Do not check the size for svg images, because Juicy Pixels cannot 41 | -- handle those. I could extract the size from the svg in a different 42 | -- way, but meh. 43 | then pure [] 44 | else fmap (either error makeDimensions) (readImage imgPath) 45 | 46 | -- Maps an IO-performing function over the attributes of all tags, and 47 | -- replaces tags with an svg source with tags instead, because 48 | -- tags cannot load svg images that contain stylesheets, whereas 49 | -- tags can. 50 | mapImgAttributes :: (Attributes -> IO Attributes) -> [Html.Tag] -> IO [Html.Tag] 51 | mapImgAttributes f = foldrM mapTag [] 52 | where 53 | mapTag tag more = case tag of 54 | (S.TagOpen "img" attrs) | ".svg" `isSuffixOf` getSrc attrs -> 55 | pure $ 56 | (S.TagOpen "object") 57 | [ ("type", "image/svg+xml") 58 | , ("data", getSrc attrs) 59 | , ("role", "image") 60 | ] 61 | -- Alt-text turns int content of the tag. 62 | : (S.TagText $ fromJust $ lookup "alt" attrs) 63 | : (S.TagClose "object") 64 | : more 65 | 66 | (S.TagOpen "img" attrs) | otherwise -> do 67 | newAttrs <- f attrs 68 | pure $ (S.TagOpen "img") newAttrs : more 69 | 70 | otherTag -> pure $ otherTag : more 71 | 72 | -- Extract "src=" attributes from images, stripping the "/images/" prefix from 73 | -- the path. 74 | getSrcPaths :: [Html.Tag] -> [FilePath] 75 | getSrcPaths tags = 76 | let 77 | appendSrc (S.TagOpen "img" attrs) srcs = getSrc attrs : srcs 78 | appendSrc _ srcs = srcs 79 | in 80 | fmap (fromJust . stripPrefix "/images/") $ foldr appendSrc [] tags 81 | 82 | -- Sets the width and height attributes of all tags, turn tags for 83 | -- svg images into tags. 84 | addDimensionsAll :: FilePath -> [Html.Tag] -> IO [Html.Tag] 85 | addDimensionsAll imgDir = mapImgAttributes $ addDimensions imgDir 86 | 87 | isImgCloseTag :: Html.Tag -> Bool 88 | isImgCloseTag tag = case tag of 89 | S.TagClose "img" -> True 90 | _ -> False 91 | 92 | -- Given a piece of html, adds the image dimensions to the attributes of every 93 | -- tag and ensures that there are no closing tags. Returns a list 94 | -- of referenced image file paths, and the new html. 95 | processImages :: FilePath -> String -> IO ([FilePath], String) 96 | processImages imgDir html = 97 | let 98 | tags = filter (not . isImgCloseTag) $ Html.parseTags html 99 | srcPaths = getSrcPaths tags 100 | in do 101 | newHtml <- fmap Html.renderTags $ addDimensionsAll imgDir tags 102 | pure (srcPaths, newHtml) 103 | -------------------------------------------------------------------------------- /src/Minification.hs: -------------------------------------------------------------------------------- 1 | -- Copyright 2015 Ruud van Asseldonk 2 | -- 3 | -- This program is free software: you can redistribute it and/or modify 4 | -- it under the terms of the GNU General Public License version 3. See 5 | -- the licence file in the root of the repository. 6 | 7 | module Minification (minifyCss, minifyHtml) where 8 | 9 | import Data.Char (isSpace) 10 | import qualified Text.HTML.TagSoup as S 11 | 12 | import qualified Html 13 | 14 | type Tag = Html.Tag 15 | 16 | -- Removes the first character of a string if that character is whitespace 17 | stripBegin :: String -> String 18 | stripBegin [] = [] 19 | stripBegin (c:str) = if (isSpace c) then str else c:str 20 | 21 | -- Removes the last character of a string if that character is whitespace 22 | stripEnd :: String -> String 23 | stripEnd = reverse . stripBegin . reverse 24 | 25 | -- Collapses adjacent whitespace to a single whitespace character. 26 | -- (The appended "x" zips with the last character to ensure it is not dropped.) 27 | mergeWhitespace :: String -> String 28 | mergeWhitespace str = fmap fst $ filter shouldKeep $ zip str $ (tail str) ++ "x" 29 | where shouldKeep (a, b) = not $ (isSpace a) && (isSpace b) 30 | 31 | mapWithPrevious :: (Maybe a -> a -> b) -> [a] -> [b] 32 | mapWithPrevious f xs = fmap (uncurry f) $ zip (Nothing : fmap Just xs) xs 33 | 34 | mapWithNext :: (a -> Maybe a -> b) -> [a] -> [b] 35 | mapWithNext f xs = fmap (uncurry f) $ zip xs ((tail $ fmap Just xs) ++ [Nothing]) 36 | 37 | filterWithPrevious :: (Maybe a -> a -> Bool) -> [a] -> [a] 38 | filterWithPrevious f xs = fmap snd . filter (uncurry f) $ zip (Nothing : fmap Just xs) xs 39 | 40 | filterWithNext :: (a -> Maybe a -> Bool) -> [a] -> [a] 41 | filterWithNext f xs = fmap fst . filter (uncurry f) $ zip xs ((tail $ fmap Just xs) ++ [Nothing]) 42 | 43 | -- Determines for every character whether it is inside a /* */ comment. 44 | identifyComments :: String -> [Bool] 45 | identifyComments = identify False 46 | where identify _ ('/' : '*' : more) = True : True : (identify True more) 47 | identify _ ('*' : '/' : more) = True : True : (identify False more) 48 | identify s (_ : xs) = s : (identify s xs) 49 | identify _ [] = [] 50 | 51 | -- Removes /* */ comments. 52 | stripCssComments :: String -> String 53 | stripCssComments css = fmap fst $ filter (not . snd) $ zip css (identifyComments css) 54 | 55 | -- Removes whitespace after a colon, semicolon, comma, curly brackets, 56 | -- parentheses, or after an angle bracket. 57 | stripCssAfter :: String -> String 58 | stripCssAfter = filterWithPrevious shouldKeep 59 | where shouldKeep (Just p) c = not $ (isSpace c) && (p `elem` ",:;{}()>") 60 | shouldKeep _ _ = True 61 | 62 | -- Removes whitespace before a curly bracket or angle bracket, and the last 63 | -- semicolon before a closing bracket. 64 | stripCssBefore :: String -> String 65 | stripCssBefore = filterWithNext shouldKeep 66 | where shouldKeep s (Just '>') = not $ isSpace s 67 | shouldKeep s (Just '{') = not $ isSpace s 68 | shouldKeep ';' (Just '}') = False 69 | shouldKeep _ _ = True 70 | 71 | -- A basic css minifier that merges and removes whitespace. The transformations 72 | -- it makes might not be correct (inside strings for example), but it works for 73 | -- the stylesheets that I use it on. 74 | minifyCss :: String -> String 75 | minifyCss = stripBegin . stripEnd 76 | . stripCssBefore . stripCssAfter 77 | . mergeWhitespace . stripCssComments 78 | 79 | -- Applies the mapping to all tags except when the tag is inside a
 tag,
 80 | -- or when the tag is inside the title. (The page title contains 
tags that 81 | -- might be hidden, and in that case there should still be whitespace to 82 | -- separate words.) 83 | applyTagsExceptPre :: ([Tag] -> [Tag]) -> [Tag] -> [Tag] 84 | applyTagsExceptPre = Html.applyTagsWhere $ not . (Html.isPre `orFns` Html.isH1) 85 | where orFns f g x = (f x) || (g x) 86 | 87 | -- Applies f to all tags except when the tag is inside a
 tag.
 88 | mapTagsExceptPre :: (Tag -> Tag) -> [Tag] -> [Tag]
 89 | mapTagsExceptPre = Html.mapTagsWhere (not . Html.isPre)
 90 | 
 91 | -- Applies f to all tags and their predecessors, except inside a 
 tag.
 92 | mapTagsPreviousExceptPre :: (Maybe Tag -> Tag -> Tag) -> [Tag] -> [Tag]
 93 | mapTagsPreviousExceptPre f = applyTagsExceptPre $ mapWithPrevious f
 94 | 
 95 | -- Applies f to all tags and their successors, except inside a 
 tag.
 96 | mapTagsNextExceptPre :: (Tag -> Maybe Tag -> Tag) -> [Tag] -> [Tag]
 97 | mapTagsNextExceptPre f = applyTagsExceptPre $ mapWithNext f
 98 | 
 99 | -- Applies a function to the text of a tag if the other tag exists and
100 | -- satisfies a condition.
101 | mapTextIf :: (Tag -> Bool) -> Maybe Tag -> (String -> String) -> Tag -> Tag
102 | mapTextIf cond (Just other) f tag = if (cond other) then Html.mapText f tag else tag
103 | mapTextIf _    Nothing      _ tag = tag
104 | 
105 | -- Strips whitespace after an opening tag.
106 | stripAfterOpen :: Maybe Tag -> Tag -> Tag
107 | stripAfterOpen prev tag = mapTextIf S.isTagOpen prev stripBegin tag
108 | 
109 | -- Strips whitespace before a closing tag.
110 | stripBeforeClose :: Tag -> Maybe Tag -> Tag
111 | stripBeforeClose tag next = mapTextIf S.isTagClose next stripEnd tag
112 | 
113 | -- Strips whitespace before an opening tag if the tag is not inline.
114 | stripBeforeOpen :: Tag -> Maybe Tag -> Tag
115 | stripBeforeOpen tag next = mapTextIf shouldStripBefore next stripEnd tag
116 |   where shouldStripBefore (S.TagOpen name _) = not $ isInline name
117 |         shouldStripBefore _ = False
118 | 
119 | -- Strips whitespace after a closing tag if the tag is not inline.
120 | stripAfterClose :: Maybe Tag -> Tag -> Tag
121 | stripAfterClose prev tag = mapTextIf shouldStripAfter prev stripBegin tag
122 |   where shouldStripAfter (S.TagClose name) = not $ isInline name
123 |         shouldStripAfter _ = False
124 | 
125 | -- Tests whether an element is inline. The list here is not exhaustive. The
126 | -- elements have been chosen such that significant whitespace is not removed
127 | -- from the html that I feed through the minifier (my rendered blog posts).
128 | isInline :: String -> Bool
129 | isInline t = t `elem` ["a", "abbr", "code", "em", "span", "strong", "sub", "sup", "time", "var"]
130 | 
131 | -- Removes comment tags and merges adjacent text tags.
132 | removeComments :: [Tag] -> [Tag]
133 | removeComments = merge . filter (not . S.isTagComment)
134 |   where merge (S.TagText u : S.TagText v : more) = merge $ (S.TagText $ u ++ v) : more
135 |         merge (tag : more) = tag : (merge more)
136 |         merge [] = []
137 | 
138 | -- Minifies the contents of all 
19 |   
20 | 


--------------------------------------------------------------------------------
/templates/index.html:
--------------------------------------------------------------------------------
 1 | {{include head.html}}
 2 |   
 3 |     
4 |
5 |

Ruud van Asseldonk

6 |
7 |

8 | Hi, I am Ruud. 9 | I love to create beautiful things with elegant code. 10 | I particularly like low-level optimisation for bare metal performance and systems programming, 11 | but I also enjoy high-level functional programming. 12 | Sometimes I write about that. 13 | I am mildly allergic to buzzwords. 14 |

15 | 20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /templates/math.css: -------------------------------------------------------------------------------- 1 | /* Make sure that subscripts and superscripts do not disrupt the baseline. The 2 | * fallback fonts for var elements can also disrupt the baseline, fix that. 3 | * Because these elements are never on their own on a line, I can get away with 4 | * setting the line height to 0. */ 5 | sub, sup, var 6 | { 7 | line-height: 0; 8 | font-family: 'Calluna Sans'; 9 | } 10 | 11 | sub, sup 12 | { 13 | word-spacing: 0; 14 | 15 | /* Add a little space before superscripts, because the symbol that comes 16 | * before it is usually in italics, so it needs a bit of correction. In some 17 | * cases (especially for the number '2'), if the previous character is not 18 | * italic, this actually makes things worse, but on average it improves the 19 | * spacing. */ 20 | margin-left: 0.1em; 21 | } 22 | 23 | sub 24 | { 25 | /* Subscripts after italics can move a bit to the left. All but one of the 26 | * subscripts that are not after an italic happen to be after the 27 | * double-struck F for my posts, so there there is room as well. The one case 28 | * where the default margin is right is a trade off. */ 29 | margin-left: -0.1em; 30 | } 31 | -------------------------------------------------------------------------------- /templates/post.html: -------------------------------------------------------------------------------- 1 | {{include head.html}} 2 | 3 |
4 |
5 |

{{header}}

6 | {{if part}}

Part {{part}}

{{end}} 7 | {{if subheader}}

{{subheader}}

{{end}} 8 |

9 | written by Ruud van Asseldonk
10 | published 11 |

12 |
13 | {{content}} 14 |
15 |
16 |
17 |

More words

18 |

{{further.title-html}}

19 |

{{further.synopsis-html}} Read full post

20 |
21 |
22 | {{include footer.html}} 23 | 24 | 25 | -------------------------------------------------------------------------------- /tools/bazelsvg.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Simple script that parses the output of "bazel analyze-profile --dump=raw" 4 | # and prints an svg bar chart to stdout. 5 | 6 | # Copyright 2018 Ruud van Asseldonk 7 | # 8 | # This program is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License version 3. See 10 | # the licence file in the root of the repository. 11 | 12 | 13 | import sys 14 | from typing import Dict, List, NamedTuple, Set 15 | 16 | def ns_to_ds(time_ns: int) -> int: 17 | """Return time rounded to deciseconds.""" 18 | return (time_ns + 50_000_000) // 100_000_000 19 | 20 | 21 | class Bar(NamedTuple): 22 | x: int 23 | y: int 24 | w: int 25 | 26 | # Critical path information is all the way at the end, and does not reference 27 | # the actions by id. We cross-reference them by description instead. 28 | def get_critical_path(fname: str) -> Set[str]: 29 | with open(fname, 'r') as f: 30 | critical_path_id = '' 31 | descriptions: Set[str] = set() 32 | 33 | for line in f: 34 | thread_id, task_id, parent_id, start_ns, duration_ns, stats, ttype, description = line.split('|') 35 | 36 | if ttype == 'CRITICAL_PATH': 37 | critical_path_id = task_id 38 | 39 | if parent_id == critical_path_id: 40 | if description.startswith("action '"): 41 | # Turn "action 'foobar'\n" into "foobar". 42 | description = description[len("action '"):-2] 43 | descriptions.add(description) 44 | 45 | return descriptions 46 | 47 | 48 | def render_bars(fname: str, start_y: int) -> List[str]: 49 | # Collect bars in a dictionary to deduplicate them, there was some overlap 50 | # that prevented the critical path from being visible because other bars were 51 | # on top. The dictionary deduplicates them. Value is the color of the bar. 52 | bars: Dict[Bar, str] = {} 53 | 54 | critical_path = get_critical_path(fname) 55 | 56 | with open(fname, 'r') as f: 57 | tids = {} 58 | tid_i = 0 59 | start_x = None 60 | 61 | for line in f: 62 | thread_id, task_id, parent_id, start_ns, duration_ns, stats, ttype, description = line.split('|') 63 | 64 | # Exclude subtasks because we already draw the parent, apart from the 65 | # critical path components, which are children of the critical path. 66 | if parent_id != '0': 67 | continue 68 | 69 | if ttype in {'SKYFRAME_EVAL', 'CREATE_PACKAGE', 'SKYFUNCTION', 'SKYLARK_USER_FN'}: 70 | continue 71 | 72 | start_ns = int(start_ns) 73 | start_dsec = ns_to_ds(start_ns) 74 | excess_ns = start_ns - start_dsec * 100_000_000 75 | 76 | duration_dsec = ns_to_ds(int(duration_ns) - excess_ns) 77 | if duration_dsec <= 3: 78 | continue 79 | 80 | start_dsec = ns_to_ds(int(start_ns)) 81 | 82 | if start_x is not None: 83 | start_dsec -= start_x 84 | else: 85 | start_x = start_dsec 86 | start_dsec = 0 87 | 88 | if thread_id not in tids: 89 | tids[thread_id] = tid_i % 8 90 | tid_i += 1 91 | tid = tids[thread_id] 92 | 93 | is_on_critical_path = description.strip() in critical_path 94 | 95 | # Deduplicate bars by coordinates. The critical path takes priority. 96 | bar = Bar(start_dsec, 7 - tid, duration_dsec) 97 | if is_on_critical_path: 98 | bars[bar] = 'fill="#c35"' 99 | elif bar not in bars: 100 | bars[bar] = 'class="bar"' 101 | 102 | return [ 103 | f'' 105 | for bar, color in sorted(bars.items()) 106 | ] 107 | 108 | fname_before, fname_after = sys.argv[1:] 109 | bars_before = render_bars(fname_before, 7) 110 | bars_after = render_bars(fname_after, 287) 111 | 112 | # We show 144 seconds in the graph, the last target finishes at 141.7 seconds, 113 | # but 144 is a multiple of 36. # On max width the image is 36em wide, which 114 | # means that 4 units in the svg coordinate system are 1 em. The line height is 115 | # 1.4em, and I want to fit two bars on a line, so I should make every bar 2.8 116 | # high. Also multiply everything by 10, so we don't have to include the decimal 117 | # dot, that saves a few bytes. 118 | print(f'', end='') 119 | 120 | # Minified style sheet template that will be rendered by the generator to 121 | # substitute the font hashes. This way we can refer to the same font as the html 122 | # body. 123 | print( 124 | "", 131 | end='' 132 | ) 133 | 134 | for t in range(0, 144, 10): 135 | # Lines in my math.css are 0.08em, those go well with the text stroke width, 136 | # and 1 em is 4 units, so the stroke width for the time grid is 0.32 units. 137 | print(f'', end='') 138 | 139 | for bar in bars_before + bars_after: 140 | print(bar, end='') 141 | 142 | for t in range(0, 144, 10): 143 | print(f'{t}', end='') 144 | 145 | print('', end='') 146 | -------------------------------------------------------------------------------- /tools/grid.js: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Ruud van Asseldonk 2 | // 3 | // This program is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License version 3. See 5 | // the licence file in the root of the repository. 6 | 7 | // Paste this into a js console to add a baseline grid to the page. 8 | 9 | var container = document.createElement('div'); 10 | container.style.position = 'absolute'; 11 | container.style.top = '0px'; 12 | container.style.left = '0px'; 13 | container.style.width = '100%'; 14 | 15 | var height = document.body.getClientRects()[0].height; 16 | document.body.insertBefore(container, document.body.firstChild); 17 | 18 | var i = 0; 19 | var h = 0; 20 | while (h < height) 21 | { 22 | var block = document.createElement('div'); 23 | block.style.backgroundColor = 'cyan'; 24 | block.style.opacity = 0.2; 25 | block.style.position = 'absolute'; 26 | block.style.top = (i * 1.4) + 'em'; 27 | block.style.left = '0px'; 28 | block.style.width = '100%'; 29 | block.style.height = '1.4em'; 30 | 31 | container.appendChild(block); 32 | i += 2; 33 | h += 2 * block.getClientRects()[0].height; 34 | } 35 | -------------------------------------------------------------------------------- /tools/scale.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Scale 6 | 68 | 69 | 70 |
0.25
1.15
71 |
0.35
1.05
72 |
0.5
0.9
73 |
0.7
0.7
74 |
1.0
0.4
75 |
1.4
1.4
76 |
2.0
0.8
77 |
2.8
1.4
78 |
4.0
0.2
79 | 80 | 81 | -------------------------------------------------------------------------------- /tools/stats.awk: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Ruud van Asseldonk 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License version 3. See 5 | # the licence file in the root of the repository. 6 | 7 | # This script is intended to be invoked from stats.sh. 8 | 9 | { 10 | size[i++] = $1; 11 | total += $1; 12 | } 13 | END { 14 | mean = total / i; 15 | 16 | if (i % 2) 17 | { 18 | median = size[int(i / 2)]; 19 | } 20 | else 21 | { 22 | median = size[i / 2] + size[i / 2 + i]; 23 | } 24 | 25 | printf(" %6.f %6.f\n", median, mean); 26 | } 27 | -------------------------------------------------------------------------------- /tools/stats.sh: -------------------------------------------------------------------------------- 1 | #/bin/sh 2 | 3 | # Copyright 2016 Ruud van Asseldonk 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License version 3. See 7 | # the licence file in the root of the repository. 8 | 9 | # Run this script from the root of the repository after generating the size to 10 | # print statistics about the size of the generated files. 11 | 12 | print_stats() 13 | { 14 | echo -n "$1" 15 | find out -name $2 | xargs du -b | sort -n | awk -f tools/stats.awk 16 | } 17 | 18 | total_size() 19 | { 20 | find out -name $1 | xargs du -bc | tail -1 | awk '{print $1}' 21 | } 22 | 23 | # Table with stats per file type. 24 | 25 | echo 'Type Median Mean' 26 | echo '---------- ------ ------' 27 | print_stats 'html ' '*.html' 28 | print_stats 'html gzip ' '*.html.gz' 29 | print_stats 'html br ' '*.html.br' 30 | print_stats 'all woff ' '*.woff' 31 | print_stats 'all woff2 ' '*.woff2' 32 | print_stats 'body woff ' 'r[0-9a-f]*.woff' 33 | print_stats 'body woff2' 'r[0-9a-f]*.woff2' 34 | 35 | # Average page weight. (No medians here because matching the html file to the 36 | # font file after generation is hard. No image sizes included because averaging 37 | # the image cost over all pages makes no sense.) 38 | 39 | n_pages=$(find out -name '*.html' | wc -l) 40 | 41 | html_weight=$(total_size '*.html') 42 | gzip_weight=$(total_size '*.html.gz') 43 | br_weight=$(total_size '*.html.br') 44 | woff_weight=$(total_size '*.woff') 45 | woff2_weight=$(total_size '*.woff2') 46 | image_weight=$(du -bc out/images/* | tail -1 | awk '{print $1}') 47 | 48 | woff_page_weight=$(expr $html_weight + $woff_weight) 49 | woff2_page_weight=$(expr $html_weight + $woff2_weight) 50 | gz_woff_page_weight=$(expr $gzip_weight + $woff_weight) 51 | gz_woff2_page_weight=$(expr $gzip_weight + $woff2_weight) 52 | br_woff2_page_weight=$(expr $br_weight + $woff2_weight) 53 | 54 | echo 55 | echo "Mean page weight (woff): $(expr $woff_page_weight / $n_pages)" 56 | echo "Mean page weight (woff2): $(expr $woff2_page_weight / $n_pages)" 57 | echo "Mean page weight (gz + woff): $(expr $gz_woff_page_weight / $n_pages)" 58 | echo "Mean page weight (gz + woff2): $(expr $gz_woff2_page_weight / $n_pages)" 59 | echo "Mean page weight (br + woff2): $(expr $br_woff2_page_weight / $n_pages)" 60 | --------------------------------------------------------------------------------