Intro
Have you ever loaded data from a CSV, and needed to create a class that can describe each row of that CSV? You've got 30+ columns, and now you're getting into your favorite language, you're so ready to make that class. You start to realize that this just sucks, writing massive classes sucks. Now you're really questioning everything, why are we here, just to write classes.
If you've read any of my other posts, you might've picked up on my focus on languages, so how would we fix this with language design?
Where Most Languages Go Wrong
Let's make a ball class in a few languages. This class is going to have an x coordinate, a y coordinate, a radius, and it will also have a method to move the ball. Let's implement that in Python
class Ball:
def __init__(self, x, y, radius):
self.x = x
self.y = y
self.radius = radius
def move(self, dx, dy):
self.x += dx
self.y += dy
Pretty simple, now let's create the same class in C#.
public class Ball
{
public float x;
public float y;
public Ball(float fX, float fY)
{
x = fX;
y = fY;
}
public void move(float dx, float dy)
{
x += dx;
y += dy;
}
So what's wrong with these?
Well, we're writing the same thing twice! look at the Python implementation, we pass x
, y
, and radius
into the class, and then assign it to x
, y
, and radius
. What a waste of characters! You may think this is stupid, but now imagine you've got 30 properties. In Python, the arguments for __init__
would be 3 page widths, and then you would have 30 lines! And in C# we would have the same issue, but twice!
Golang & Kotlin
Let's see how Golang and Kotlin has created a fix to this.
type Ball struct {
x float64
y float64
radius float64
}
func (b *Ball) move(dx, dy float64) {
b.x += dx
b.y += dy
}
That looks nice, right? We're not doing the same thing twice!
data class Ball(val x: Float, val y: Float, val radius: Float)
Look, I don't know Kotlin, but I remember seeing this and thinking "wow, what a great idea."
What's The Problem?
Let's have a look back at the Golang example, and now we can imagine that we want a print that we're creating the object, well now we have to create some sort of constructor function.
func createBall(x, y, radius float64) Ball {
fmt.Println("Created Ball")
return Ball{x, y, radius}
}
look how much we're repeating now! So now we have the real issue.
- When we want a very static experience, we often have to write the same thing twice or three times
- As soon as we want constructor logic, no matter the language, we are forced into more redundant logic
The Solution
As someone who has designed a lot of languages in my spare time, I can't not design a solution.
class Ball {
x float64
y float64
radius float64
init(b Ball) Ball {
println("Created Ball")
}
}
What's good about this solution? Well, we can imagine when we call the Ball.init
function, that a zeroed ball is constructed, such that x is 0, y is 0, and radius is 0. We then pass this Ball
into the Ball.init
function, run the code, and return it. What would calling this function look like?
ball = Ball()
There is no duplicate logic here, but there are two more issues I want to address, and how we should fix them.
Changing Properties
Let's say we want to create a ball with 3 as its x
, well, that's simple.
ball = Ball(3)
Now if we're to do y
, there are 2 possible solutions.
ball = Ball(0, 3)
Or, alternatively
ball = Ball(y: 3)
This makes it easy t target the specific property you want to change. The second option is extremely useful for changing one property on an object with 30 properties.
Now what about defining default values?
class Ball {
x float64: 3
y float64: 7
radius float64
init(b Ball) Ball {
println("Created Ball")
}
}
That wasn't so hard, was it? Now when we create a new Ball
, it will automatically have x
as 3, and y
as 7. radius
will stay as 0.
Pointer Methods
This is the second issue, and a personal annoyance (especially against Python). I hate implicit pointers, which is why I like Golang's syntax as such.
func (b *Ball) move(dx, dy float64) {
b.x += dx
b.y += dy
}
You can see that we pass b
into the function as a pointer by the obvious *
. In our solution, we want to make it obvious when we're doing this as well. Now, I would probably just end up at the same solution as Go, but here is an example for the move
function.
class Ball {
x float64: 3
y float64: 7
radius float64
init(b Ball) Ball {
println("Created Ball")
}
(b *Ball) move(dx, dy float64) {
b.x += dx
b.y += dy
}
}
Conclusion
I think for all practical purposes, we've made the most efficient class definition, or at least the concept. The ideas can be used, regardless is squirlies, methods inside the class declaration, and so on.
I'm not thinking about this for no reason, if you read my recent post I'm creating an interpreted language inside of my game. Hopefully, one day we can all create our classes and structs much easier. None of this means that the above languages have bad solutions, but in my mind, this is the best solution.
Top comments (0)