Jump to page: 1 2
Thread overview
difficulties with const structs and alias this / template functions
Nov 18, 2018
Dennis
Nov 18, 2018
Stanislav Blinov
Nov 18, 2018
Dennis
Nov 19, 2018
Stanislav Blinov
Nov 19, 2018
Dennis
Nov 19, 2018
Stanislav Blinov
Nov 19, 2018
Stanislav Blinov
Nov 18, 2018
Rubn
Nov 19, 2018
Dennis
Nov 19, 2018
Stanislav Blinov
Nov 19, 2018
Dennis
Nov 19, 2018
Stanislav Blinov
Nov 19, 2018
Dennis
Nov 19, 2018
Stanislav Blinov
Nov 19, 2018
Dennis
Nov 19, 2018
Stanislav Blinov
November 18, 2018
I'm making a fixed point numeric type and want it to work correctly with const. First problem:

```
const q16 a = 6;
a /= 2;          // compiles! despite `a` being const.
writeln(a);      // still 6
a.toQ32 /= 2;    // what's actually happening
```

My q16 type has an implicit conversion to q32 (like how int can be converted to long):
```
q32 toQ32() const {
  return q32(...);
}
alias toQ32 this;
```
How do I make it so that a const(q16) will be converted to a const(q32) instead of mutable q32?

Second problem:
```
Q log2(Q)(Q num) if (is(Q : q16) || is(Q : q32)) {
    import std.traits: Unqual;
    Unqual!Q x = num;
    // actual code
}
```
When I call this with a const(q16), Q is resolved to const(q16) so I have to unqualify Q every time. It works, but feels clumsy. Is there an easier way to automatically de-const parameters? We're working with small value types here, it should be simple.

If anyone knows any other pitfalls with const, I'd love to know them.
November 18, 2018
On Sunday, 18 November 2018 at 17:30:18 UTC, Dennis wrote:
> I'm making a fixed point numeric type and want it to work correctly with const. First problem:
>
> ```
> const q16 a = 6;
> a /= 2;          // compiles! despite `a` being const.

Ouch. That's actually kind of nasty.

> writeln(a);      // still 6
> a.toQ32 /= 2;    // what's actually happening
> ```
>
> My q16 type has an implicit conversion to q32 (like how int can be converted to long):
> ```
> q32 toQ32() const {
>   return q32(...);
> }
> alias toQ32 this;
> ```
> How do I make it so that a const(q16) will be converted to a const(q32) instead of mutable q32?

Like this:

        // implement separate methods for mutable/const/immutable
        q32 toQ32() {
            return q32(x);
        }

        const(q32) toQ32() const {
            return q32(x);
        }

        immutable(q32) toQ32() immutable {
            return q32(x);
        }

Or like this:

        // implement all three in one method, using the `this template` feature
        auto toQ32(this T)() {
            static if (is(T == immutable))
                return immutable(q32)(x);
            else static if (is(T == const))
                return const(q32)(x);
            else
                return q32(x);
        }


> Second problem:
> ```
> Q log2(Q)(Q num) if (is(Q : q16) || is(Q : q32)) {
>     import std.traits: Unqual;
>     Unqual!Q x = num;
>     // actual code
> }
> ```
> When I call this with a const(q16), Q is resolved to const(q16) so I have to unqualify Q every time. It works, but feels clumsy. Is there an easier way to automatically de-const parameters? We're working with small value types here, it should be simple.

Define different overloads for Q and const Q. Or this:

Q log2(Q)(inout Q num) if (is(Q : q16) || is(Q : q32)) { /* ... */ }

Being able to jam mutable/const/immutable implementation in one function like that should tell you that you shouldn't mutate the argument. Then, the necessity to Unqual will go away on it's own ;)
November 18, 2018
On Sunday, 18 November 2018 at 18:17:54 UTC, Stanislav Blinov wrote:
>         // implement separate methods for mutable/const/immutable

Thanks. I should have tried that, but I assumed it wouldn't work since you can't overload on return-type only. However, the const / non-const makes it allowed.

> Or like this:
>
>         // implement all three in one method, using the `this template` feature

That's new to me, interesting.

> Define different overloads for Q and const Q. Or this:
>
> Q log2(Q)(inout Q num) if (is(Q : q16) || is(Q : q32)) { /* ... */ }
>
> Being able to jam mutable/const/immutable implementation in one function like that should tell you that you shouldn't mutate the argument. Then, the necessity to Unqual will go away on it's own ;)

Different overloads sounds like a lot of boilerplate.
inout still results in "cannot modify `inout` expression `input`"
My goal is to be able to write straightforward and correct signatures for fixed point functions that receive mutable copies of whatever they are fed. This isn't even limited to my custom types:

```
T f0(T)(T x, T y) {return x += y;}
int  f1(int  x, int  y) {return x += y;}
long f1(long x, long y) {return x += y;}

void main()
{
    import std.stdio;
    writeln(f0(const(int)(3), const(long)(4))); // can't modify const
    writeln(f1(const(int)(3), const(long)(4))); // fine
}
```
The explicit overloads of f1 work fine, but the generic f0 does not. I could just use the f1 strategy of explicitly making overloads, but then adding a q64 or q128 type would mean having to double the existing boilerplate everywhere. I would like to have a `isFixedPoint` template and generic functions that take any isFixedPoint!Q, but it seems I have to manually enforce "don't forget to unqual Q first!".

November 18, 2018
On Sunday, 18 November 2018 at 17:30:18 UTC, Dennis wrote:
> I'm making a fixed point numeric type and want it to work correctly with const. First problem:
>
> ```
> const q16 a = 6;
> a /= 2;          // compiles! despite `a` being const.
> writeln(a);      // still 6
> a.toQ32 /= 2;    // what's actually happening
> ```
>
> My q16 type has an implicit conversion to q32 (like how int can be converted to long):
> ```
> q32 toQ32() const {
>   return q32(...);
> }
> alias toQ32 this;
> ```
> How do I make it so that a const(q16) will be converted to a const(q32) instead of mutable q32?
>
> Second problem:
> ```
> Q log2(Q)(Q num) if (is(Q : q16) || is(Q : q32)) {
>     import std.traits: Unqual;
>     Unqual!Q x = num;
>     // actual code
> }
> ```
> When I call this with a const(q16), Q is resolved to const(q16) so I have to unqualify Q every time. It works, but feels clumsy. Is there an easier way to automatically de-const parameters? We're working with small value types here, it should be simple.
>
> If anyone knows any other pitfalls with const, I'd love to know them.

Yah most people tend to avoid const for this reason. It only really works for basic types, if you have a "const int" you can convert it to an "int" by copy. But if you have a type like Vector!(const int) that won't work, you can't even convert Vector!int to Vector!(const int) easily for example.

> ```
> Q log2(Q)(Q num) if (is(Q : q16) || is(Q : q32)) {
>     import std.traits: Unqual;
>     Unqual!Q x = num;
>     // actual code
> }
> ```

This is pretty much the only way, you can just add

alias V = Unqual!Q;

then use V in your function instead of Unqual!Q everywhere.
November 19, 2018
On Sunday, 18 November 2018 at 22:30:52 UTC, Rubn wrote:
> Yah most people tend to avoid const for this reason. It only really works for basic types, if you have a "const int" you can convert it to an "int" by copy. But if you have a type like Vector!(const int) that won't work, you can't even convert Vector!int to Vector!(const int) easily for example.

That's unfortunate. I can relate better to Jonathan's article [1] now. But I'll still try to make it working with const since other people might want to use it.

I'm also trying to make it work with immutable, and from BigInt [2] I learned that constructors need to be `pure` for creating immutable objects. (I don't know why.)

Are there any other gotchas? I didn't add an immutable variant for toQ32 like Stanislav suggested, but creating an immutable q32 from a q16 still seems to work fine.

[1] http://jmdavisprog.com/articles/why-const-sucks.html
[2] https://issues.dlang.org/show_bug.cgi?id=17330
November 19, 2018
On Sunday, 18 November 2018 at 20:10:52 UTC, Dennis wrote:
> On Sunday, 18 November 2018 at 18:17:54 UTC, Stanislav Blinov wrote:

>> Q log2(Q)(inout Q num) if (is(Q : q16) || is(Q : q32)) { /* ... */ }
>>
>> Being able to jam mutable/const/immutable implementation in one function like that should tell you that you shouldn't mutate the argument. Then, the necessity to Unqual will go away on it's own ;)
>
> Different overloads sounds like a lot of boilerplate.
> inout still results in "cannot modify `inout` expression `input`"

You just dismissed that second to last sentence, did you? :)

> My goal is to be able to write straightforward and correct signatures for fixed point functions that receive mutable copies of whatever they are fed. This isn't even limited to my custom types:
>
> ```
> T f0(T)(T x, T y) {return x += y;}
> int  f1(int  x, int  y) {return x += y;}
> long f1(long x, long y) {return x += y;}
> ```
> The explicit overloads of f1 work fine, but the generic f0 does not.

```
T f0(T)(inout T x, inout T y) { return x + y; }
```

;)

But if you really really want to mutate the argument, then handling different mutability for T is the only way:

```
T f0(T)(T x, T y) {
    import std.traits : isMutable;
    static if (isMutable!T) return x += y;
    else return x + y;
}
```

I know it's a bit painful though. In fact, Phobos also suffers from it. In std.numeric:

T gcd(T)(T a, T b)
if (isIntegral!T)
{
    static if (is(T == const) || is(T == immutable))
    {
        return gcd!(Unqual!T)(a, b);
    }
    // ...
}

Not only that looks ugly, but (with DMD) it makes gcd a doulbe function call :D
November 19, 2018
On Monday, 19 November 2018 at 00:50:28 UTC, Dennis wrote:

> I'm also trying to make it work with immutable, and from BigInt [2] I learned that constructors need to be `pure` for creating immutable objects. (I don't know why.)

That's only for types with indirections (pointers), since `pure` guarantees that you do not mutate any global state.

For value types, they work just fine:

struct q16 {
    uint x;

    this(uint x) { this.x = x; }
    this(uint x) const { this.x = x; }
    this(uint x) immutable { this.x = x; }

    // or again, all three packed together:

    this(this T)(uint x) { this.x = x; }
}

> Are there any other gotchas? I didn't add an immutable variant for toQ32 like Stanislav suggested, but creating an immutable q32 from a q16 still seems to work fine.

Yup, that's because, like Rubn said, copying value types is trivial. Where it all comes to bite you is when you start having pointers, because you can't copy a const(T)* into a T*.

November 19, 2018
On Monday, 19 November 2018 at 01:13:29 UTC, Stanislav Blinov wrote:
> You just dismissed that second to last sentence, did you? :)

I don't know what you mean with it. It's not that I'm trying to be sneaky or lazy really trying to modify the const parameter when I should treat it properly. And as the author I'm fine with Unqualing everything if that's needed, my concern is when another person using my type tries to write his own function:

```
q16 pow(q16 base, int exponent) {
  q16 result = 1;
  foreach(i; 0..exponent) result *= base;
  return result;
}

const q16 x = 3;
writeln(pow(x, 3)); //works!
```

He then wants to make it more generic, so he rewrites:

```
Q pow(Q base, int exponent) if (isFixedPoint!Q) {
  Q result = 1;
  foreach(i; 0..exponent) result *= base;
  return result;
}
```

And initially it seems to work, but as soon as it is used with const it breaks as `result` can't be mutated anymore. I'd like to set the example of writing proper generic functions, and if there is something simpler than importing Unqual I'd prefer that over my current solution. If there isn't, I'll just need to write a "don't forget to Unqual const" comment.

> ```
> T f0(T)(inout T x, inout T y) { return x + y; }
> ```
>
> ;)

What does inout do here? If the return type is also inout(T) I know that the return type gets the same qualifiers as inout parameters. But in this example, the return value is still T.

> I know it's a bit painful though. In fact, Phobos also suffers from it. In std.numeric:

Yuck!

November 19, 2018
On Monday, 19 November 2018 at 01:24:02 UTC, Stanislav Blinov wrote:
> Yup, that's because, like Rubn said, copying value types is trivial. Where it all comes to bite you is when you start having pointers, because you can't copy a const(T)* into a T*.

I'm not using reference types, but still:

```
struct S {
    int a;
    this(int a) {
        this.a = a;
    }
}

void main()
{
    immutable S d = 3;
}

```

onlineapp.d(10): Error: mutable method onlineapp.S.this is not callable using a immutable object
onlineapp.d(10):        Consider adding const or inout to onlineapp.S.this

const still leaves the first error, inout works though I don't know what it does.
Adding pure also works.
November 19, 2018
On Monday, 19 November 2018 at 02:03:18 UTC, Dennis wrote:
> On Monday, 19 November 2018 at 01:13:29 UTC, Stanislav Blinov wrote:
>> You just dismissed that second to last sentence, did you? :)
>
> I don't know what you mean with it. It's not that I'm trying to be sneaky or lazy really trying to modify the const parameter when I should treat it properly. And as the author I'm fine with Unqualing everything if that's needed, my concern is when another person using my type tries to write his own function:
>
> ```
> q16 pow(q16 base, int exponent) {
>   q16 result = 1;
>   foreach(i; 0..exponent) result *= base;
>   return result;
> }
>
> const q16 x = 3;
> writeln(pow(x, 3)); //works!
> ```
>
> He then wants to make it more generic, so he rewrites:
>
> ```
> Q pow(Q base, int exponent) if (isFixedPoint!Q) {
>   Q result = 1;
>   foreach(i; 0..exponent) result *= base;
>   return result;
> }
> ```
>
> And initially it seems to work, but as soon as it is used with const it breaks as `result` can't be mutated anymore.

Yes, but that's not the problem with your type. It's a problem with the user not realizing that const is a type qualifier.

> I'd like to set the example of writing proper generic functions, and if there is something simpler than importing Unqual I'd prefer that over my current solution. If there isn't, I'll just need to write a "don't forget to Unqual const" comment.

>> ```
>> T f0(T)(inout T x, inout T y) { return x + y; }
>> ```
>>
>> ;)
>
> What does inout do here?

You're right, it's not needed there at all.

> If the return type is also inout(T) I know that the return type gets the same qualifiers as inout parameters. But in this example, the return value is still T.

Because, as before, value types are all copyable between mutable/const/immutable. So even if `return x + y` would yield a `const T`, you can still instantiate a T from it.

« First   ‹ Prev
1 2