2020 August 15th
Languages inform how we conceive and communicate ideas. Linguistic influence hypothesizes how language speakers' may perceive the world differently based on their language. As a software developer, my daily challenge is to convert ideas into communication that machines understand. Just as human culture and language influence the approach to communication, the language used to communicate with machines shapes the solutions to software problems. Programming languages are designed to make some types of machine instruction easier (often to the detriment of other scenarios).
Learning additional languages will force you to approach problems in a new way. I want to highlight two languages that changed my daily processes - hopefully convincing you to do some exploration yourself 🤞.
Typescript is a superset of Javascript. Which itself is an implementation of ECMAScript. Frontend, ah the sweet complexities of frontend.
Although I had written some HTML, the first time that I truly began programming was with an ECMAScript language learning to create Flash games. Little did I know that many years later my first job would be writing Flash games in ActionScript 3! Eventually, I transitioned to Javascript for web development, solidly engraining ECMAScript as a mainstay for my career.
Javascript's default inclusion in browsers has made it the tool of choice for interactivity on the web. The typeless language has never confined itself to a single programming paradigm. These freedoms make it a tool for nearly every situation but come with the trade-off where "bad code" is as easy to write as "good code". Beyond this, developing for the web requires knowledge of many technologies (HTML, CSS, Javascript) and the ability to orchestrate them. A web application will need to store data (a database) and send that data across devices. Each of these components directly translates to increasing complexity.
Enter Typescript. Typescript is a syntactic superset of Javascript. It adds a typesystem and tooling to provide confidence in the shape of your code. Javascript's unrestrictive design lets you solve problems in this complex ecosystem, but it does so with little guarantee of safety. Typescript brings safety to the overly-complex world of web development.
1// Maxfunction in javascript2function max(a, b) {3return a > b ? a : b;4}56// function in typescript7function max(a: number, b: number) {8return a > b ? a : b;9}10
In this simple example, both functions return the larger of our two input arguments. However, the Typescript example adds types for the parameters. Now the tooling (IDE and build tools) can determine the return type, as well as check the usage of the >
operator! We are guaranteed that our max
function will return a number - since a
and b
are numbers.
Typescript shone as I was building forms for a React side project. I want users to be able to submit their data, but I also want to be able to create forms quickly (and safely). The app uses GraphQL for its API which means we can generate our types automatically. Using Typescript generics and generated GraphQL types we can infer the required form fields and a typed response from our API!
1/* Additional typing for passing a generated mutation hook to our form.2Apollo MutationHooks return the structure [mutateFunction, response] */3type MutationHook<Vars, Data> = (options?: {4variables: Vars;5}) => [MutateFunction<Vars>, {data?: Data}];6
We can then associate the same generics from the MutationHook to the properties for the Form.
1interface FormProps<Vars, Data> {2mutationHook: MutationHook<Vars, Data>;3onSuccess?(data: Data): void;4}5
Now when using the form, Typescript can infer the types from our concrete Mutation. If our onSuccess
function didn't consume the correct type of data we would get an error.
1<Form2mutationHook={AddTodoHook}3onSuccess={(data: AddTodoMutationPayload) => console.log(data)}4/>5
This safety is what I learned from Typescript. In the unwieldy world of web, Typescript makes things fit together. I talk about the benefit of using Typescript to develop confidence in my blog post about testing.
Elixir is a dynamic, functional, and concurrency-first programming language. Elixir is born of Erlang concurrency, a bit of Ruby syntax, and a sprinkle of functional features from Clojure.
When I first learned about Elixir, I had spent a year working at a rails-shop falling in love with the simplicity of the Rails framework and elegance of the Ruby language. Unfortunately, I had also been faced with the difficulties of scaling the application. Over the years I grew to love it so much that it was my language of choice for Halp — which has now battle-tested Elixir in production for the past year.
As a functional language, Elixir demands a different programming paradigm than most developers use frequently. For one, data is immutable and once data is declared it cannot be changed. This immutability is a worthy trade-off and part of what makes Elixir so performant and concurrency-friendly. The second major difference is that everything is an expression. Expressions differ from statements (instructions) in that they have a result.
13 + 6 // an expression2print('Hello, World!') // a statement3
Since everything is an expression, Elixir can also support a pipe operator similar to pipelines in Unix. The result of your expression will be sent forward and used as the first argument for the next call.
1String.split(String.upcase("Hello World")) # ["HELLO", "WORLD"]23# or with Elixir's pipeline operator4"Hello World" |> String.upcase() |> String.split() # ["HELLO", "WORLD"]5
Finally, my favourite language feature is pattern-matching (destructuring). If you have a map or list you can describe the shape that you are expecting and pull out certain keys or indices. Elixir will throw an error if no matches are found.
1["hello", second] = ["hello", "world"] # second = "world"2["hello", second] = ["goodbye", "world"] # throws an error since "hello" != "goodbye"34{:ok, result} = {:ok, "File updated"} # result = "File updated"5{:ok, _} = {:ok, "Ignored value"} # wildcard _ to ignore the rest of the tuple, throws an error if not :ok6
Putting these pieces together makes for a scalable application but the programming experience makes for happy developers along the way.
1# pattern match our arguments to make sure we receive a map with "user" and grab the params out of the map2def create(%{"user" => user_params}) do3# Repo.insert is going to return a map starting with {:ok, DATABASE_ENTRY} or {:error, SOME_ERROR}45%User{} # create an empty user object6|> User.changeset(user_params)7|> Repo.insert()8|> case do # pass the result of insert into our case (switch) statement9{:ok, result} -> # destructure the new user into result10render("user.html", result)1112{:error, _} -> render("error.html") # ignore the type of error13end14# everything is an expression so case returns its result (rendered html)15end16
Elixir is worth learning for the drastic shift in approach alone. The benefits of a modern language with concurrency and performance are all just bonuses. Further, the language is flexible but does a great job of shepherding users towards a happy path by being just opinionated enough.
In The Pragmatic Programmer the authors encourage readers to learn a new language every year. I think this idea is sound and timeboxing it provides a solid heuristic for investing in your growth. I would propose one amendment though: try a new language every year. Focus on application and effort, not mastery. This way, a new language can be a shortcut to new perspectives.
For me, I want to learn more Rust. Typescript and Elixir are dynamic languages with an emphasis on functional paradigms and Rust will be a change of pace with its approach to memory. Hopefully, you'll be reading a blog post about Rust and game development soon.
Thank you for reading. Thank you for exploring ideas with me.