Caveat Emptor: I am no expert in any of these languages. DO NOT read this post and assume any such thing.
Picking up where I left off with Hello World in D, Go and Rust in VS Code, this post restarts the thread of my personal learning and exploration of these three programming languages:
Of these three, only D is really an OOP language, but I wanted to see how close I could get in Go and Rust. I started with D and then morphed and adapted as I moved to Go and finally to Rust. The code in this experiment is overly simplistic and meant only to explore basic OOP principles. Nothing in this code is meant to demonstrate best practices in any of these languages.
Update All
First things first. Like my blog, the system on which I wrote the previously noted post has been neglected these past few months. I updated VS Code from June 2019 v1.26.1 to October 2019 v1.40.2. I had Auto Update enabled, so my extensions were updated as well: DLang to v 1.16.7, Go to 0.11.9, and Rust (rls) to 0.7.0.
Next I updated my language environments by downloading and installing the latest releases for Windows:
*dirty: Yeah, I wondered about that too. Apparently it is an artifact of their build system after moving to GitHub. Nothing to worry about. Probably.
The D installer did ask for some VC runtime redistributables or and install of Visual Studio. I already had Visual Studio 2019 installed but would later remember that it had been uninstalled and I had not installed the VC++ dev options. This left no VC redistributables on my machine apparently, as I would soon learn when trying to use the Rust compiler.
Validate Prior Code
The D and Go compilers worked with the prior post’s Hello World code just fine. The Rust compiler complained like this:
PS D:\langc\rust\Hello> rustc app.rs
error: linker `link.exe` not found
|
= note: The system cannot find the file specified. (os error 2)
note: the msvc targets depend on the msvc linker but `link.exe` was not found
note: please ensure that VS 2013, VS 2015, VS 2017 or VS 2019 was installed with the Visual C++ option
error: aborting due to previous error
This was easily resolved by closing VS Code, opening Visual Studio Installer, and adding Desktop development with C++ in Workloads. Opened VS Code again and ran rustc app.rs and the problem was solved.
The app.exe grew by a few bytes for D and Rust but shrunk just a little for Go. I’m not certain why but I thought that was interesting.
Code in GitHub
Rather than listing out all the code in this post, I put the old Hello World code and the new code for this post into https://github.com/tylerjensen/d-go-rust which should make it easier for you to see the effort as a whole. There is nothing special about this repo. It’s just a learning playground and definitely does not purport to be a demonstration of proper practices in any language, let alone best. So reader beware!
Object Oriented Programming
There is no shortage of opinions available on the web as to what constitutes “Object Oriented Programming” but there are certainly some comment themes. Here are the few principles that I selected to use in my attempt to build and compare some OOP code in D, Go and Rust. This is not a comprehensive list and definitely not an attempt to authoritatively define OOP.
- Objects / Classes — data structures and methods (or functions)
- Encapsulation — data and logic protection (private vs. public)
- Composition — support for “has a” relationships
- Inheritance — support for “is a” relationships
- Polymorphism — an object that “is a” thing can be used as a thing
- Open Recursion — a thing can call itself and access its data (this or self)
There may be many other elements of OOP and some of them may be important to you, but for now, let’s see if we can limit it to the scope defined in the bullets above, at least for this foray into learning three new languages.
General Approach
To keep things simple, I’m limiting each effort to one or just a few files. Implementation of methods/functions are simplistic console outputs. Only one complexity was added because I wanted to see how each language deals with date and time. As simple as date/time may seem to humans, it’s another thing entirely to a computer and each language has its own way of dealing with the variety of RFC standards for date/time data.
Another effort I’ve made to keep this simple is that each example is built as a simple console application. No libraries built. No HTTP listener. No windows. No command line arguments. No real architectural organization or realistic naming or code documentation. This is an effort to learn the basics of the languages rather than their best practices. I’ll leave that to the experts in each language.
I’ve tried to write the same code in each language. Mostly. There are differences, especially with the Rust code. The Rust compiler is rather opinionated and to eliminate warnings, I’ve complied with its suggestions.
Here’s the list items I’ve tried to create in each code base:
- Interface Printable with Print method
- Abstract class Person that implements Printable interface
- Demographics class that has a DOB date and encapsulates age behind a GetAge method
- An Employee class that inherits from Person and composes a Demographics instance
- An overridden Print method in the Employee class
- Overloaded methods that take a Person, Employee and Printable as an argument to examine polymorphism
OOP in D
I chose to start in D because it really is an OOP language where Go and Rust, technically speaking, are not. This would give me a simpler entry point into building some code that looks like the kind of OOP that I’m used to seeing in C#. This would allow me to cover most of the elements of OOP that I wanted to explore.
The D code is split into three files, each defining a “module” because within a given module, all private and public class members are accessible to all others. This means that to ensure encapsulation, you have to use separate modules which are defined by the module keyword and follow the convention that the module name is the same as the file name.
I’m not going to show all the code here, but here’s the “per” module which has the interface, abstract class, and the composable class defined. It’s not all that different from my C-based languages including my favorite C#.
module per;
import std.stdio;
import std.datetime;
public interface Printable
{
void Print();
}
public abstract class Person : Printable
{
public void Print()
{
writeln("Person printed.");
}
public string Name;
}
public class Demographics
{
private int age;
public Date DateOfBirth;
this(Date dob)
{
DateOfBirth = dob;
}
public int GetAge()
{
SysTime today = Clock.currTime;
// Example of open recursion - 'this'
this.age = today.year - DateOfBirth.year;
return age;
}
}
The rest of the code can be found here: https://github.com/tylerjensen/d-go-rust/tree/master/D/oop
It’s rather self explanatory for anyone familiar with C-based languages. I won’t spend much time explaining the code. My lazy approach to looking at overloading and polymorphism can be found in the app.d file in the poly methods.
OOP in Go
The first thing an OOP developer runs into with Go is that it has no concept of a class. Rather data defined in a “struct” and those structs can have functions added to them outside the scope of the struct using what Go calls a “receiver” meaning that the function extends the struct with behavior. Encapsulation is easy. A struct member that begins with a capital letter is public. A lowercase letter is private. But similar to D, unless that struct lives in a separate package, the private member is accessible. Unlike D, a package must be in it’s own directory, so you will find the per.go and emp.go files in their own subdirectories.
Go does have a concept of an interface but it does have have an explicit declaration of an object implementing that interface. There are some advantages to this and if you assign a pointer of an instance of a struct to an interface variable and your struct does not implement receiver functions that comply with the interface, the compiler will spit that back out at you.
Here’s the same code as shared above for D. The per.go file is fairly easy to read. In fact, I quite like Go’s readability.
package per
import (
"fmt"
"time"
)
type Printable interface {
Print()
}
type Person struct {
Name string
}
func (p Person) Print() {
fmt.Printf("Person printed.\n")
}
type Demographics struct {
age int
DateOfBirth time.Time
}
func (d Demographics) GetAge() int {
t := time.Now()
d.age = t.Year() - d.DateOfBirth.Year()
return d.age
}
And honestly, I’m not missing those semi-colons.
As you peruse the code https://github.com/tylerjensen/d-go-rust/tree/master/Go/oop, you will see that in app.go that Go does not support overloaded functions (methods), unless they are receiver functions for different types but that’s outside the scope of this post. It does allow you to declare a variable of type interface and then assign it a pointer to a struct variable that implements that interface in receiver functions.
OOP in Rust
Of all three languages, I had more trouble with Rust. It’s paradigms are more foreign to my C# mind, but that’s okay. In most respects, it is very similar to Go but has no garbage collector and relies on a lighter, faster approach called ownership and borrowing. I’m not sure if we call that OB or BO. Either way it is a constraint that does make you think more about resource usage and what you’re doing with your data structures.
Rust does support a concept of an interface but they call it a trait. Encapsulation was easier but I ran into trouble thinking I needed to do something similar to Go’s packages using Rust modules (“mod” keyword). I could not get the trait defined in one module and used in another. I did find some documentation and feature requests that indicate that this is not supported. So I dropped back to a single file and one module.
Here’s a partial listing of app.rs found here https://github.com/tylerjensen/d-go-rust/blob/master/Rust/oop/app.rs
pub trait Printable {
fn print(&self);
}
pub struct Person {
pub name: String
}
impl Printable for Person {
fn print(&self) {
println!("Person Print {}", self.name);
}
}
pub struct Demographics {
// Cannot make this private and not require initialization
age: i32,
pub date_of_birth: chrono::DateTime<chrono::offset::Utc>
}
impl Demographics {
pub fn calc_age(self: &mut Self) -> i32 {
self.age = Utc::now().year() - self.date_of_birth.year();
return self.age;
}
}
You can see that Rust does have explicit implementation for a trait (interface) and that you must pass a reference of the struct to a function of an “impl” code block for a given struct. This is definitely different than Go. But if you look carefully, you’ll see that if the implementation is going to change data in the struct, you must pass “self” as a mutable pointer. This means that calling the “calc_age” function can only be done by an instance that is declared at mutable using the “mut” keyword. If you mess up on this, the compiler will let you know.
Conclusion
I don’t know what’s next but I’m having fun. I don’t think I will continue to try to push Go and Rust into the OOP hole. They have strengths that are worth exploring without being constrained by an approach the languages were not designed to support. Exploring and learning their best practices could be fun.
Each language has its strengths and quirks. Go is certainly more popular and Rust is growing. The D language remains rather small in terms of market penetration and is nurtured by a smaller group of very dedicated computer scientists. I do not know if it will survive. Go and Rust definitely will as their communities are much larger.
More fun to come. Stay tuned.