DEV Community

Chig Beef
Chig Beef

Posted on

Perfect Class Syntax, Every Language Falls Short

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
Enter fullscreen mode Exit fullscreen mode

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;
}           
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

That looks nice, right? We're not doing the same thing twice!

data class Ball(val x: Float, val y: Float, val radius: Float)
Enter fullscreen mode Exit fullscreen mode

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}
}
Enter fullscreen mode Exit fullscreen mode

look how much we're repeating now! So now we have the real issue.

  1. When we want a very static experience, we often have to write the same thing twice or three times
  2. 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")
    }
}
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Now if we're to do y, there are 2 possible solutions.

ball = Ball(0, 3)
Enter fullscreen mode Exit fullscreen mode

Or, alternatively

ball = Ball(y: 3)
Enter fullscreen mode Exit fullscreen mode

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")
    }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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)