How Programming Languages Change How You Think
source link: https://killalldefects.com/2020/12/27/how-programming-languages-change-how-you-think/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
How Programming Languages Change How You Think
This might be a weird article.
A few years ago I saw the terrific movie “Arrival” and it changed how I look at language. A little later, I encountered F# for the first time and it changed the way I look at programming languages.
In this article I’m going to walk you through the basics of some of the theories touched upon in Arrival and then pivot into the role of language in programming. We’ll compare and contrast a number of languages and see how they impact how we think about solving problems in software engineering before we do a deeper dive into the F# programming language.
I told you this might be a weird article.
The first half of the article will cover a wide range of languages while the second half will focus on F# and how the structure and syntax of that languages impacts our approaches to programming.
The Impact of Language on Thought
Let’s start with a brief journey into science fiction.
Note: This section contains mild spoilers on the first hour of the plot in Arrival, most of which could be gleaned from the film’s trailer.
The 2016 film Arrival is a science fiction drama movie starring Amy Adams and Jeremy Renner. It’s the kind of movie that I love but my wife sleeps through – a slower-paced movie where a group of scientists try to communicate with aliens who have arrived over various parts of the world.
The key focus of Arrival is language and specifically how language impacts the way we form thoughts and even perceive reality.
The aliens that the scientists encounter mainly communicate via logograms containing entire sentences in a circular pattern as pictured below:
There’s a beautiful sequence roughly 55 minutes into the film where they discuss the alien language and its problems at length and it includes this quote from one of our main scientists:
“If you immerse yourself into a foreign language then you can actually rewire your brain”
The sequence goes on to discuss the Sapir-Whorf hypothesis and if the language you speak can alter how you think or, in the case of the characters in Arrival, how they perceive reality.
As you might expect, the film enters the territory of mind-bending science fiction shortly thereafter, but it’s a very good film if that’s your cup of tea.
While I enjoyed the film and its concepts, it got me thinking about programming and how different programming languages might alter the way we think about application development.
This is a thought that stuck with me and resurfaced as I delved deeper into functional programming.
The Impact of Programming Language on Thought
Let’s take a quick tour of a few different programming languages. We’ll look at some sample code from each one and speculate at the ways of thinking that they might encourage or discourage.
Disclaimer: I am including basic information on several languages I have not extensively programmed in to offer a healthy variety of syntax. I will be relying on some speculation for these as well as my own personal experiences with the languages my knowledge and experience are deeper in.
C# is an imperative programming language built to support object-oriented programming (OOP) with syntax inspired by C++ and Java. Like F# and Visual Basic, C# runs on .NET and can interact with other .NET programming languages.
Let’s look at some sample C# code from a game project of mine from years back:
Here we have a class from a game context that contains an ApplyDamage method. Code flows from top to bottom and may or may not enter if statements for conditional logic before it returns a number indicating the damage dealt.
This type of programming is very typical for object oriented languages despite the simplified example here and encourages the following types of behavior:
- Thinking about the system in terms of individual objects
- Focusing on if statements which may or may not need to execute
- Adding properties, methods, and inheritance to manage complexity as things grow
I should note here that although C# was built as an object-oriented interpreted language, it is shifting to allow the language to support functional programming approaches like F#.
There are some notable differences to C#, however.
It turns out that type declarations are actually a pretty big deal to how you think about your code. Instead of wondering what type something is or having to rely on accurate documentation, you can now see the type declaration next to the variable or parameter. This is a nice touch, however it does have drawbacks.
Because TypeScript relies on the programmer to accurately define the types, it pushes you towards types that are easy to represent via standard syntax. This in turn discourages you from passing functions around as parameters.
If you were to declare a parameter that is a function that takes in a Number and returns a Boolean, that syntax is possible but still difficult to remember, write, and read, so you naturally tend to do it less frequently.
Code example taken from The Algorithms
As someone who has only done bits of Python programming, I can’t speak too much to how Python changes your way of thought, except to point out how elegant the syntax feels compared to other languages.
It’s a very minimal style requiring indentation over brackets to define scopes. This reduces the level of noise in the code and helps you focus on the logic.
In my experience, this style of minimalism causes me to emulate it with very concise functions that call out to other functions, resulting in code that is nearly as compact as F#, but significantly more readable.
Go (also called GoLang) is a more recent development and represents another statically typed imperative programming language.
Let’s look at its syntax because it’s a bit different in a few aspects beyond just where braces and brackets are placed:
Code example taken from The Algorithms
Go has a few syntax niceties I’ve not bumped into in other languages yet.
The for keyword is much more flexible, allowing for infinite loops if needed. Additionally, Go uses := as a shorthand for declare and assign, which keeps noise to a minimum. Finally, we see a quick way of swapping two values with the , and = operators.
I cannot write much how Go might impact how we think about writing applications, but I’d be curious about the experiences of those of you who have worked more with Go.
Everything I’ve covered so far has primarily been an imperative programming language. F#, by contrast, is a functional programming language, which relies on entirely different patterns of code:
Code example taken from The Algorithms
F#, like C#, is strongly typed and those types are enforced by the compiler. However, more type information is inferred by the F# compiler and so actual type keywords are only needed sparingly. As a result, F# syntax is able to work more seamlessly with complex types, since the need to represent those types in syntax is significantly reduced.
F# syntax often deals with “piping” things from function to function to bring about an expected result.
We’ll take a more in-depth look at F# in the next section and examine a few cases involving piping before exploring a wider range of ways its different syntax may impact how you think about problem solving in code.
Note: F# is not the only modern functional programming language, but out of my familiarity with the F# language and the fact that this article is technically part of the community F# advent 2020 series, we’ll zero in F# for the remainder of the article.
If you’d like to look more into the syntax related to many of the currently used programming languages, I recommend you check out The Algorithms on GitHub for an open source set of examples including some other functional programming languages.
I’d also love to see your thoughts on how programming languages have shaped how you think about programming, so please leave a comment as well.
How F# Changed How I think about Programming
With this overview of languages aside, let’s dive deeper into the F# language and how its features change the way we approach code.
F# is not C#
When I first tried to learn F#, it was a difficult experience for me. It was my first functional programming language and terms such as monads, partial application of functions, and higher-order functions made my journey a difficult one.
Adding to that burden, as I learned F#, I was focused on “How do I write this C# code in F#?” so my first attempts at F# tried to create strong types with mutable state like you’d do in a C# program.
While F# can do that, it’s not what F# is about. F# isn’t about creating classes with mutable state and traditional members but about writing functions and composing those functions into solutions to larger problems.
In object-oriented languages we focus on encapsulating logic in cohesive objects and calling methods on those objects in a specific sequence. In other words, the classes are the stars of the show and their methods help things work.
F# encourages puts the focus on individual functions and pushes you towards immutable state and chaining functions together. The functions are the main attraction and the types that support them are left as minimal as possible (though F#’s typing system is very powerful and flexible).
Thinking in Terms of Pipes
When I think of F# applications, I picture a series of pipes connected to each other – each one meticulously designed to have exactly the correct output given a specific input and have no other side effects. This type of function is commonly called a pure function.
The individual pipes (or functions) are small and highly specialized, but together they make a very capable application.
F# as a language is very good at chaining things together thanks to its specialized operators such as the |> or “pipe” operator which takes the value on the left and makes it the last parameter to the function on the right.
Just looking at the presence of this one isolated operator in F# a multitude of things become easy in F# that would be harder in a language such as C#.
For example, look at the following F# code:
watermelon |> SliceOpen |> RemoveSeeds
This takes whatever watermelon is and passes it as a parameter to the
SliceOpen function and then takes the result of that call and passes it in to the
We could simplify this further in F# by using the
>> operator to compose a
SliceAndRemoveSeeds function from the
RemoveSeeds functions like this:
let SliceAndRemoveSeeds = SliceOpen >> RemoveSeeds
watermelon |> SliceAndRemoveSeeds
If we wanted to do the same thing in C# code, we’d have to write the following:
var slicedWatermelon = SliceOpen(watermelon);
var preppedWatermelon = RemoveSeeds(slicedWatermelon);
Admittedly, we could simplify this a bit by writing:
var preppedWatermelon = RemoveSeeds(SliceOpen(watermelon));
This does work, but you can start to feel the language fighting you a little and now need to learn to read your code from the inside out. F# code is much more of a sequential and minimal syntax (though it does take some getting used to), so this kind of functional chaining code becomes more natural in F# vs C#.
Because the pipe operator always passes in the result as the last parameter to the function it is piped into, we now need to start paying attention to the order in which we declare parameters.
The pipe operator is not alone in this regard either since things like partial application of functions also have an impact on how you order parameters:
let AddNumbers x y = x + y
let AddTwo = AddNumbers 2
let five = AddTwo 3
Here we define
AddTwo as a function that calls
AddNumbers with the first parameter of 2 and the second parameter unspecified. Next, we can call
AddTwo with a second parameter of 3 which ultimately adds 2 and 3 together.
The resulting effect is a somewhat new and unusual feeling of needing to think about your parameters and understand which one ought to be last.
The F# compiler’s intelligence comes at a cost: In F# not only do you have to pay attention to which function is defined first within a file in order for things to compile, you also need to explicitly set a compilation order for individual files inside of your project.
For example, in the picture below, the
Gasses.fs file can reference anything in
Positions.fs, but cannot reference anything in
GameObjects.fs or below.
This process of needing to consider file and function order initially seems like an irritant. However, it has some pleasant side effects.
Because functions are now order dependent it becomes impossible to create infinite loops of function A calling function B which calls function A again and your module dependencies are now much easier to visualize. The compiler effectively ensures your dependencies only flow in a specific direction. Entire tools such as NDepend have been built around trying to enforce this concept in other codebases.
All of these features I’ve described above are mere syntax sugar to some of F#’s true strengths, however.
F# offers some truly interesting mechanisms for declaring types. Take this GameObjectType definition from a side project of mine:
This is similar to a C# enum but offers some additional bits of logic. Here a game object can be any number of things, but a few things have additional data associated with them. An air pipe needs to track the mixture of gasses flowing through it, for example, and a door is either going to be left to right or top to bottom in a 2D world and will either be open or closed.
In an object-oriented language we might use inheritance and define subclasses for these doors and pipes, but in many cases this might be overkill – particularly when we want our focus to be on concise, reusable functions instead of powerful objects.
We can then
match off of the object type in F#:
This is a case where F#’s syntax has pushed us away from inheritance and towards using an intelligent match keyword and expressions to handle the various cases (with _ being a wildcard meaning anything else that didn’t match).
As a result, our focus as developers remains on the functions instead of being on creating powerful types. This in turn makes us focus on how our functions fit together.
Options over Nulls
Nulls have famously been called the billion dollar mistake. While I’m not sure I go that far, it’s worth pointing out F#’s handling of nulls to the uninitiated.
Here’s a small F# function that tries to find a 2D game tile at a specific position inside of another tile layer. It will either find a tile or it won’t, and if it does, it needs to merge that tile on top of the current tile:
F# chooses to handle nulls primarily with the option type. An option is either going to be some value or it’s going to be none. Option could be considered to be a generic type in C# contexts and will never be null itself.
The F# compiler insists that you use specific ways of getting the value out of an option and explicitly makes you handle the possibility of an option being none. This forces you as a developer to explicitly handle these cases and reduces the odds of you accidentally forgetting to handle nulls.
It should be noted that other languages are also starting to adopt these approaches, most notably with C# adding support for explicit null checking as an opt-in feature.
What F# has to do with George Orwell’s 1984
Before we close, let’s look briefly at another work of fiction and it’s thoughts on language: George Orwell’s classic novel 1984.
I read this book a number of times in my youth, and one of the most striking aspects of it was language and how the “Ministry of Truth” in the book had revised speech to make undesirable concepts harder to represent. For example, words such as “bad” were replaced with things like “ungood”.
It’s a striking picture and example, regardless of whether you were excited to read 1984 in school or not, and it’s come to represent how I think about the F# language.
For me the key beauty of the F# programming language is that F# makes invalid states harder to represent.
In programming, every decision – including those made by our language designers – comes with tradeoffs. Things that are easy we tend to do and things that are harder to represent we tend to avoid.
F#’s functional syntax comes with a hefty learning curve and a need to almost re-learn programming in a different context, but it does make invalid states harder to represent at the compiler level. Whether that price is worth paying is up for you and your team to decide.
Additionally, not all problems should be solved by a functional programming language. Most of programming is maintaining existing code and not all code has the complexity needed to necessitate functional programming languages. Some problem domains also have a lot of unique information and logic that truly does belong in types.
Whichever programming language you choose to use in your project, it should serve your purpose and it should be a conscious decision on your part.
When you start a new project ask yourself these things:
- What does this language make easy for me?
- What does this language make harder?
- How hard is it to find or train developers who can write and maintain this language?
- How much training would my team need to use this language?
- How much support on this language can I expect from official and community channels in the future?
- How hard is it for me to use this code to work with other existing code our organization uses?
- How much does this language provide for me and what would I need to add myself?
- Do I like the type of code this language encourages me to write?
We need to start examining the tradeoffs of language choices and what patterns our languages are pushing us into, because these choices may matter more than you realize.
This article is part of the F# Advent 2020 series. Check that series out for more articles on F# from members of the community.
Written by Matt Eland
Aggregate valuable and interesting links.
Joyk means Joy of geeK