Jump to page: 1 2
Thread overview
March 12

I am in need of a data type for holding direction information; one of 8 directions on a single axis. They are named in terms of compass directions. If D had a 4-bit datatype, I would just use this and do +=2 whenever I want to change the datatype, but it doesn't.

Perhaps this would be a good programming challenge for someone more experienced than me. Make a data type (probably a struct) for holding one of 8 directional values using 3 bits. It should accept the use of increment operators to change the angle.

Ideally (but optionally), it should work as an enum of the same name; "Direction".

Here's a unittest that it should ideally pass:

unittest
{
    Direction direction = Direction.N;
    direction++;
    assert(direction == Direction.NE);
    direction+=3;
    assert(direction == Direction.S);
    direction--;
    assert(direction == Direction.SE);
    direction-=4;
    assert(direction == Direction.NW);
}
March 12
By taking advantage of integer wrapping and a bitwise and, its quite a simple problem to solve!

Challenge for the reader: add support for binary operations and toString support.

https://dlang.org/spec/operatoroverloading.html

```d
struct Direction {
    private int value;

    Direction opUnary(string op:"++")() {
        value++;
        value &= 7;
        return this;
    }

    Direction opUnary(string op:"--")() {
        value--;
        value &= 7;
        return this;
    }

    void opOpAssign(string op:"+")(int amount) {
        value += amount;
        value &= 7;
    }

    void opOpAssign(string op:"-")(int amount) {
        value -= amount;
        value &= 7;
    }

    enum Direction N = Direction(0);
    enum Direction NE = Direction(1);
    enum Direction E = Direction(2);
    enum Direction SE = Direction(3);
    enum Direction S = Direction(4);
    enum Direction SW = Direction(5);
    enum Direction W = Direction(6);
    enum Direction NW = Direction(7);
}

unittest {
     Direction direction = Direction.N;
     direction++;
     assert(direction == Direction.NE);
     direction+=3;
     assert(direction == Direction.S);
     direction--;
     assert(direction == Direction.SE);
     direction-=4;
     assert(direction == Direction.NW);
}
```
March 12
On Tuesday, 12 March 2024 at 06:38:28 UTC, Richard (Rikki) Andrew Cattermole wrote:
> By taking advantage of integer wrapping and a bitwise and, its quite a simple problem to solve!
>
> Challenge for the reader: add support for binary operations and toString support.
>
> https://dlang.org/spec/operatoroverloading.html
>
> ```d
> struct Direction {
>     private int value;
>
>     Direction opUnary(string op:"++")() {
>         value++;
>         value &= 7;
>         return this;
>     }
>
>     Direction opUnary(string op:"--")() {
>         value--;
>         value &= 7;
>         return this;
>     }
>
>     void opOpAssign(string op:"+")(int amount) {
>         value += amount;
>         value &= 7;
>     }
>
>     void opOpAssign(string op:"-")(int amount) {
>         value -= amount;
>         value &= 7;
>     }
>
>     enum Direction N = Direction(0);
>     enum Direction NE = Direction(1);
>     enum Direction E = Direction(2);
>     enum Direction SE = Direction(3);
>     enum Direction S = Direction(4);
>     enum Direction SW = Direction(5);
>     enum Direction W = Direction(6);
>     enum Direction NW = Direction(7);
> }
>
> unittest {
>      Direction direction = Direction.N;
>      direction++;
>      assert(direction == Direction.NE);
>      direction+=3;
>      assert(direction == Direction.S);
>      direction--;
>      assert(direction == Direction.SE);
>      direction-=4;
>      assert(direction == Direction.NW);
> }
> ```

Interesting. I didn't know that an enum can be defined inside a struct like that. I had used functions to get around it.

Here is what I had already mostly written, using help from ChatGPT (but only for the opUnary syntax, not the algorithm):
```
struct Direction //One of 8 directions stored in 3 bits
{
    bool[3] d;

    static Direction N() { return Direction(d:[false,false,false]); }
    static Direction NE() { return Direction(d:[false,false,true]); }
    static Direction E() { return Direction(d:[false,true,false]); }
    static Direction SE() { return Direction(d:[false,true,true]); }
    static Direction S() { return Direction(d:[true,false,false]); }
    static Direction SW() { return Direction(d:[true,false,true]); }
    static Direction W() { return Direction(d:[true,true,false]); }
    static Direction NW() { return Direction(d:[true,true,true]); }

    ref Direction opUnary(string op)() if (op == "++" || op == "--") {
        if (op == "++") const bool up = true;
        else const bool up = false;

        if (d[0]) {
            if (d[1]) d[2] = !d[2];
            d[1] = !d[1];
        }
        d[0] = !d[0];
        return this;
    }

    auto to(T)() const {
        return cast(T)(d[0] + 2*d[1] + 4*d[2]);
    }
}
```

I am not entirely sure how well it works. I will come back later with an updated version with more functions.

I'm not familiar with the syntax of the line `value &= 7;`. Is it equivalent to writing `value = value % 7;`?

Anyway, you used an int, but I used an array of 3 bools. I'm guessing that mine uses less memory, but I'm not sure how memory it adds when it's a struct with functions.
March 13
On 13/03/2024 11:00 AM, Liam McGillivray wrote:
> I'm not familiar with the syntax of the line |value &= 7;|. Is it equivalent to writing |value = value % 7;|?

& is a bitwise and.

LSB 123456789 MSB

& 7

LSB 123000000 MSB

> Anyway, you used an int, but I used an array of 3 bools. I'm guessing that mine uses less memory, but I'm not sure how memory it adds when it's a struct with functions.

Due to alignment, it'll probably use just as much.

Mine only needs a single ``byte``, at 7 bits it's more than enough.

But ``int`` doesn't make much difference unless you are packing instances together ``align(0):`` and realistically cpus are optimized for 32bits not 8.
March 13

On Tuesday, 12 March 2024 at 05:38:03 UTC, Liam McGillivray wrote:

>

Perhaps this would be a good programming challenge for someone more experienced than me. Make a data type (probably a struct) for holding one of 8 directional values using 3 bits. It should accept the use of increment operators to change the angle.

D is such a perfect language that you can do the features you mentioned and more. I also implemented the following during my rookie years:

alias Direction = enumSet;
union enumSet(T)
{
  T e;

  alias e this;
  struct
  {
    int i;

    auto opUnary(string op: "++")()
      => i = i < e.max ? ++i : e.min;

    auto opUnary(string op: "--")()
      => i = i > e.min ? --i : e.max;

    auto opOpAssign(string op: "+")(int val)
    {
      i += val;
      if(i > e.max) i -= e.max + 1;
    }

    auto opOpAssign(string op: "-")(int val)
    {
      i -= val;
      if(i < e.min) i += e.max + 1;
    }
  }

  string toString() const
    => e.to!string;
}

unittest
{
     auto direction = Direction!Directions(Directions.N);
     direction++;
     assert(direction == Directions.NE);
     direction+=3;
     assert(direction == Directions.S);
     direction--;
     assert(direction == Directions.SE);
     direction-=4;
     assert(direction == Directions.NW);
}

import std.stdio, std.conv;

void main()
{
  enum Directions
  {
    N , NE , E, SE, S, SW , W, NW
  }

  auto test = enumSet!Directions(Directions.W);
       test += 9; /*

       ++test; ++test; ++test;
       ++test; ++test; ++test;
       ++test; ++test; ++test;//*/

       test.writeln;

       test--; test--;
       test.writeln;
}

Here union was used with an extra template parameter. I also added 2 aliases to avoid confusion.

SDB@79

March 13

On Tuesday, 12 March 2024 at 05:38:03 UTC, Liam McGillivray wrote:

>

I am in need of a data type for holding direction information; one of 8 directions on a single axis. They are named in terms of compass directions. If D had a 4-bit datatype, I would just use this and do +=2 whenever I want to change the datatype, but it doesn't.

Perhaps this would be a good programming challenge for someone more experienced than me. Make a data type (probably a struct) for holding one of 8 directional values using 3 bits. It should accept the use of increment operators to change the angle.

Ideally (but optionally), it should work as an enum of the same name; "Direction".

Here's a unittest that it should ideally pass:

unittest
{
    Direction direction = Direction.N;
    direction++;
    assert(direction == Direction.NE);
    direction+=3;
    assert(direction == Direction.S);
    direction--;
    assert(direction == Direction.SE);
    direction-=4;
    assert(direction == Direction.NW);
}

While there are workarounds (as proposed in the other answers, using operator overloads) I tend to think that the way currently enums work with arithmetic operators is a symptom of an incomplete type-system. Enums made of integer numbers are sub classes of the parent integral sequence.

The semantics of the operators are actually not as clear as that. What if you define

enum Direction
{
    N = 1, NE, S = 45, SW
}

?

You directly see that the proposed workarounds dont work anymore.

There are natural numbers, sub-sets of natural numbers, unordered sequences, etc.
The type system does not represent that. Anyway, I dont blame D, plenty of languages have the exact same problem.

March 13

On Wednesday, 13 March 2024 at 10:27:49 UTC, Basile B. wrote:

>

The semantics of the operators are actually not as clear as that. What if you define

enum Direction
{
    N = 1, NE, S = 45, SW
}

?

Certainly! EnumMembers; can be used. The EnumMembers template from the std.traits module is used to retrieve all the members of an enumeration. It generates a tuple containing all the enumeration values, which can be iterated over using a foreach loop. In the D programming language, you can use EnumMembers to iterate over enum values at compile time, which is useful for generating code based on enum members.
Here’s a simple code example:

 enum Days
 {
   Monday    = 1001,
   Tuesday   = 1010,
   Wednesday = 1011,
   Thursday  = 1100,
   Friday    = 1101,
   Saturday  = 1110,
   Sunday    = 1111
 }

 import std.traits : EnumMembers;
 foreach(day; EnumMembers!Days)
   day.writeln(":", cast(int)day);

SDB@79

March 14

On Tuesday, 12 March 2024 at 06:38:28 UTC, Richard (Rikki) Andrew Cattermole wrote:

>

By taking advantage of integer wrapping and a bitwise and, its quite a simple problem to solve!

Challenge for the reader: add support for binary operations and toString support.

Last night I pushed the latest commit to the GitHub repository for my game. It contains the Direction struct in source/common.d. Here it is:

struct Direction //One of 8 directions stored in 3 bits
{
    import std.conv;
    import std.traits: isNumeric;

    bool[3] b;

    static Direction N = Direction(b:[false,false,false]);
    static Direction NE = Direction(b:[true,false,false]);
    static Direction E = Direction(b:[false,true,false]);
    static Direction SE = Direction(b:[true,true,false]);
    static Direction S = Direction(b:[false,false,true]);
    static Direction SW = Direction(b:[true,false,true]);
    static Direction W = Direction(b:[false,true,true]);
    static Direction NW = Direction(b:[true,true,true]);

    ref Direction opUnary(string op)() if (op == "++" || op == "--") {
        static if (op == "++") const bool up = true;
        else const bool up = false;

        if (b[0]) {
            if (b[1]) b[2] = !b[2];
            b[1] = !b[1];
        }
        b[0] = !b[0];
        return this;
    }

    void opOpAssign(string op)(int amount) if (op == "+" || op == "-") {
        amount = amount%8;
        if (amount > 0) for (uint i = 0; i < amount; i++) {
            static if (op=="+") this++;
            else this--;
        } else for (uint i=0; i > amount; i--) {
            static if (op=="+") this--;
            else this++;
        }
    }

    T to(T)() const if(isNumeric!T) {
        return cast(T)(b[0] + 2*b[1] + 4*b[2]);
    }

    T opCast(T)() if (isNumeric!T) {
        return cast(T)(b[0] + 2*b[1] + 4*b[2]);
    }

    T to(T)() const if(is(T==string)) {
        if (this==Direction.N) return "north";
        else if (this==Direction.NE) return "northeast";
        else if (this==Direction.E) return "east";
        else if (this==Direction.SE) return "southeast";
        else if (this==Direction.S) return "south";
        else if (this==Direction.SW) return "southwest";
        else if (this==Direction.W) return "west";
        else if (this==Direction.NW) return "northwest";
        else throw new Exception("Direction.to!: direction has a value that should be impossible.");
        //else return ""; //This should never happen.
    }

    bool[3] opCast() const {
        return this.b;
    }

    Direction opposite() const {
        return Direction([b[0], b[1], !b[2]]);
    }

    bool diagonal() {
        return b[0];
    }

    int getAngle() {
        if (this==Direction.N) return 0;
        else if (this==Direction.NE) return 45;
        else if (this==Direction.E) return 90;
        else if (this==Direction.SE) return 135;
        else if (this==Direction.S) return 180;
        else if (this==Direction.SW) return 225;
        else if (this==Direction.W) return 270;
        else if (this==Direction.NW) return 315;
        else throw new Exception("Direction.getAngle: direction has a value that should be impossible.");
    }
}

The one thing that cant be done on this is doing direction+8. Perhaps it would have been easier if I had chosen to store the value as a ubyte rather than an array of bools. While this is probably over-optimized given the large amount of memory in today's computers, I like it that it can never be an illegitimate value.

There's probably some trick I can use to make it easier to figure out the functions to do numerical operations on bits, but I don't know how best to cleanly represent this kind of thing in code. Maybe I need to look for examples of how it's already done.

I noticed among the options for overloading unary operations are the symbols "+" & "-". What operation is being overloaded with the function ref Direction opUnary(string op:"+")(int amount)?

March 14
There appears to be a few things that you may not be aware of based upon this implementation.

The cost of an add + increment then a bitwise and is only 2-4 cycles on a Haswell cpu. Depending upon if its working solely in registers (via inlining) or its operating on ram.

The cost of a move from ram into a register is about 1 cycle, but can have latencies around 3 cycles if not cached.

Whereas if you need to do branching (if statement, loops), this is an unpredictable cost and loops where a simple bitwise operation can be done is out right non-optimized.

As for methods like to:

```d
static immutable string[] table = ["north", ...];
return table[this.value];
```

That's two moves from ram to register.

In essence in your design, you have blown out the cost by a significant margin well beyond the 12% that is described as being the minimum to consider optimization.

If you want to understand where the 12% number comes from I suggest reading the paper "Structured Programming with go to Statements" by Donald Knuth.

As for exceptions, totally not required. You can solve this by simply making your state private so nobody else can mutate it. Bounds checking will ensure if the state is corrupted it'll error out if you use the lookup method I suggested above.

Understanding what I have said is very important for game engine programmers. So if you're interested in it beyond some hobbyist endeavors you have some reading up to do ;)
March 14
On Thursday, 14 March 2024 at 01:58:46 UTC, Richard (Rikki) Andrew Cattermole wrote:
> The cost of an add + increment then a bitwise and is only 2-4 cycles on a Haswell cpu. Depending upon if its working solely in registers (via inlining) or its operating on ram.
>
> Whereas if you need to do branching (if statement, loops), this is an unpredictable cost and loops where a simple bitwise operation can be done is out right non-optimized.
>
> As for exceptions, totally not required. You can solve this by simply making your state private so nobody else can mutate it. Bounds checking will ensure if the state is corrupted it'll error out if you use the lookup method I suggested above.

I tried to rework the functions to use bitwise operations, but it was difficult to figure out the correct logic. I decided that it's not worth the hassle, so I just changed the value storage from `bool[3]` to `ubyte`. Now it works much more like your version.
https://github.com/LiamM32/Open_Emblem/blob/c2014ab3f77e89c0cedcd6dbf7f8362ebfac33a9/source/common.d

I did a little reading, so now I understand what it means when you have `&= 7`. But I want to ask, is this faster than `%= 8`? If not, I would like to change it to the latter for readability.
« First   ‹ Prev
1 2