www.digitalmars.com         C & C++   DMDScript  

digitalmars.D.learn - Should the "front" range primitive be "const" ?

reply Drone1h <drone1h gmail.com> writes:
Hello all,



I am trying to implement a ("struct template" ? what is the 
correct word ?) range that just forwards its primitives ("empty", 
"front", "popFront") to another range, possibly with some very 
limited filtering/alteration, as std.range.Take (just to learn).

Initially, the "front" member function (property) used to be 
declared "const", but that was not accepted when the underlying 
range (denoted "R" in the code below) was std.stdio.File.ByChunk 
("Error: mutable method std.stdio.File.ByChunk.front is not 
callable using a const object").

Is there any value in having the "front" range primitive declared 
to be a "const"  member function ?

And if so, is the following implementation okay ? Could it be 
further simplified ?

     struct Taker (R)
     {
         private R _r;

         ...

         static if (functionAttributes ! (R.front) & 
FunctionAttribute.const_)
             public  property auto front () const { return 
_r.front; }
         else
             public  property auto front ()       { return 
_r.front; }

         ...
     }



Thank you respectfully !
Drone1h
Jan 29
parent reply Jonathan M Davis <newsgroup.d jmdavisprog.com> writes:
On Tuesday, January 30, 2018 01:05:54 Drone1h via Digitalmars-d-learn wrote:
 Hello all,



 I am trying to implement a ("struct template" ? what is the
 correct word ?) range that just forwards its primitives ("empty",
 "front", "popFront") to another range, possibly with some very
 limited filtering/alteration, as std.range.Take (just to learn).

 Initially, the "front" member function (property) used to be
 declared "const", but that was not accepted when the underlying
 range (denoted "R" in the code below) was std.stdio.File.ByChunk
 ("Error: mutable method std.stdio.File.ByChunk.front is not
 callable using a const object").

 Is there any value in having the "front" range primitive declared
 to be a "const"  member function ?

 And if so, is the following implementation okay ? Could it be
 further simplified ?

      struct Taker (R)
      {
          private R _r;

          ...

          static if (functionAttributes ! (R.front) &
 FunctionAttribute.const_)
              public  property auto front () const { return
 _r.front; }
          else
              public  property auto front ()       { return
 _r.front; }

          ...
      }



 Thank you respectfully !
 Drone1h
If you want to put an attribute on it, inout is better, because then it will work with any constness, but in general, I'd suggest just avoiding the use of const or immutable altogether when dealing with ranges. front can return a const element, and that will happen if you use auto and whatever you're wrapping is const, but const ranges are utterly useless, because they can't be mutated and thus can't be iterated. As such, almost no code is ever going to have a range that is anything but mutable, which means that having front be anything but mutable is generally pointless. The design of ranges makes them fundamentally incompatible with const. We'd had to have gone with the head/tail model where you get an entirely new object every time you pop elements off if we wanted const or immutable to work. The fact that popFront pretty much outright kills const. If tail-const slicing were a thing for ranges, then we'd get something similar to tail/cdr out of the deal, and const ranges could be made to work, but it's a royal pain to do tail-const with user-defined types (especially templated types), and slicing an entire range like that isn't actually part of the range API. hasSlicing requires slicing indices but not the entire range. Slicing without indices operation is used on containers to get ranges but isn't used for ranges themselves. So, as it stands at least, I'd suggest that you simply not bother using const with ranges. - Jonathan M Davis
Jan 29
parent reply Steven Schveighoffer <schveiguy yahoo.com> writes:
On 1/29/18 8:20 PM, Jonathan M Davis wrote:
 On Tuesday, January 30, 2018 01:05:54 Drone1h via Digitalmars-d-learn wrote:
 Hello all,



 I am trying to implement a ("struct template" ? what is the
 correct word ?) range that just forwards its primitives ("empty",
 "front", "popFront") to another range, possibly with some very
 limited filtering/alteration, as std.range.Take (just to learn).

 Initially, the "front" member function (property) used to be
 declared "const", but that was not accepted when the underlying
 range (denoted "R" in the code below) was std.stdio.File.ByChunk
 ("Error: mutable method std.stdio.File.ByChunk.front is not
 callable using a const object").

 Is there any value in having the "front" range primitive declared
 to be a "const"  member function ?

 And if so, is the following implementation okay ? Could it be
 further simplified ?

       struct Taker (R)
       {
           private R _r;

           ...

           static if (functionAttributes ! (R.front) &
 FunctionAttribute.const_)
               public  property auto front () const { return
 _r.front; }
           else
               public  property auto front ()       { return
 _r.front; }

           ...
       }



 Thank you respectfully !
 Drone1h
If you want to put an attribute on it, inout is better, because then it will work with any constness, but in general, I'd suggest just avoiding the use of const or immutable altogether when dealing with ranges. front can return a const element, and that will happen if you use auto and whatever you're wrapping is const, but const ranges are utterly useless, because they can't be mutated and thus can't be iterated. As such, almost no code is ever going to have a range that is anything but mutable, which means that having front be anything but mutable is generally pointless.
Not necessarily. A main reason for const is to advertise "I'm not going to change your mutable data" on a function. So reading that front is const (or inout more appropriately) can assure you front makes this guarantee. Yes, it also allows you to call on an immutable or const range, both of which are for the most part useless. So I would say const ranges are useless, but const members of ranges provide some value. That being said, const is viral, as is inout. So unfortunately if you *don't* mark your functions const or inout, then wrappers need to take this into account. -Steve
Jan 30
next sibling parent "H. S. Teoh" <hsteoh quickfur.ath.cx> writes:
On Tue, Jan 30, 2018 at 08:54:00AM -0500, Steven Schveighoffer via
Digitalmars-d-learn wrote:
 On 1/29/18 8:20 PM, Jonathan M Davis wrote:
[...]
 If you want to put an attribute on it, inout is better, because then
 it will work with any constness, but in general, I'd suggest just
 avoiding the use of const or immutable altogether when dealing with
 ranges. front can return a const element, and that will happen if
 you use auto and whatever you're wrapping is const, but const ranges
 are utterly useless, because they can't be mutated and thus can't be
 iterated. As such, almost no code is ever going to have a range that
 is anything but mutable, which means that having front be anything
 but mutable is generally pointless.
I think you're conflating a const range, which *is* pretty useless since you can't iterate it, and a const .front, which only means "calling .front will not change the state of the range". The latter is very possible, and potentially useful. Well, there's also a .front that returns a const element, which means "you can't change the current element of the range". That's also possible, and useful.
 Not necessarily. A main reason for const is to advertise "I'm not
 going to change your mutable data" on a function. So reading that
 front is const (or inout more appropriately) can assure you front
 makes this guarantee.
 
 Yes, it also allows you to call on an immutable or const range, both
 of which are for the most part useless.
 
 So I would say const ranges are useless, but const members of ranges
 provide some value.
 
 That being said, const is viral, as is inout. So unfortunately if you
 *don't* mark your functions const or inout, then wrappers need to take
 this into account.
[...] Simen has had some ideas recently about "head mutable" aka tail-const, which could be a first step towards making const ranges, or rather tail-const ranges, actually usable: https://forum.dlang.org/post/cpxfgdmklgusodqouqdr forum.dlang.org tl;dr summary: (1) Ranges implement a standard method, tentatively called opHeadMutable, that returns a head-mutable version of themselves. For example, const(MyRange!T).opHeadMutable would return MyRange!(const(T)). (2) Standard library functions would recognize opHeadMutable and use it where needed, e.g., when you hand them a const range. (3) Profit. :-P T -- In a world without fences, who needs Windows and Gates? -- Christian Surchi
Jan 30
prev sibling next sibling parent reply Jonathan M Davis <newsgroup.d jmdavisprog.com> writes:
On Tuesday, January 30, 2018 07:49:28 H. S. Teoh via Digitalmars-d-learn 
wrote:
 On Tue, Jan 30, 2018 at 08:54:00AM -0500, Steven Schveighoffer via 
Digitalmars-d-learn wrote:
 On 1/29/18 8:20 PM, Jonathan M Davis wrote:
[...]
 If you want to put an attribute on it, inout is better, because then
 it will work with any constness, but in general, I'd suggest just
 avoiding the use of const or immutable altogether when dealing with
 ranges. front can return a const element, and that will happen if
 you use auto and whatever you're wrapping is const, but const ranges
 are utterly useless, because they can't be mutated and thus can't be
 iterated. As such, almost no code is ever going to have a range that
 is anything but mutable, which means that having front be anything
 but mutable is generally pointless.
I think you're conflating a const range, which *is* pretty useless since you can't iterate it, and a const .front, which only means "calling .front will not change the state of the range". The latter is very possible, and potentially useful. Well, there's also a .front that returns a const element, which means "you can't change the current element of the range". That's also possible, and useful.
Except that unless front returns by ref, it really doesn't matter whether front is const unless it's violating the range API, since front is supposed to return the same value until popFront is called (or if it's assigned a new value via a front that returns by ref). So, in practice, putting const on front really doesn't help you any, and it actually hurts you for range composability.
 Simen has had some ideas recently about "head mutable" aka tail-const,
 which could be a first step towards making const ranges, or rather
 tail-const ranges, actually usable:

   https://forum.dlang.org/post/cpxfgdmklgusodqouqdr forum.dlang.org

 tl;dr summary:

 (1) Ranges implement a standard method, tentatively called
     opHeadMutable, that returns a head-mutable version of themselves.
     For example, const(MyRange!T).opHeadMutable would return
     MyRange!(const(T)).

 (2) Standard library functions would recognize opHeadMutable and use it
     where needed, e.g., when you hand them a const range.

 (3) Profit. :-P
I still need to look over what he's proposing in more detail - it's been proposed before (by Andrei IIRC) that one possible solution would be to add an operator for returning a tail-const version of a type, but no one has ever taken that idea anywhere. Personally, I'm getting to the point that I'd rather just avoid const than deal with any further complications for ranges. In principle, I like the idea of const, but in practice, it just constantly gets in the way, and I've rarely actually seen any benefit from it in either C++ or D. I can think of one time in my entire life where const has prevented a bug for me - which was when I got the arguments backwards to C++'s std::copy function. At least with immutable, you get implicit sharing and some optimization opportunities. In principle, const can get you some of the optimization opportunities but only in really restricted circumstances or circumstances where you could have used immutable and the code would have been the same (e.g. with int). - Jonathan M Davis
Jan 30
parent reply Steven Schveighoffer <schveiguy yahoo.com> writes:
On 1/30/18 8:05 PM, Jonathan M Davis wrote:
 
 Except that unless front returns by ref, it really doesn't matter whether
 front is const unless it's violating the range API, since front is supposed
 to return the same value until popFront is called (or if it's assigned a new
 value via a front that returns by ref). So, in practice, putting const on
 front really doesn't help you any, and it actually hurts you for range
 composability.
Right, but that is the difference between a convention ("front is supposed to...") vs. a compiler-enforced guarantee (modifying data by calling a const-tagged front is a compiler error). If you are OK with conventions, you don't need const at all. -Steve
Jan 31
parent reply Jonathan M Davis <newsgroup.d jmdavisprog.com> writes:
On Wednesday, January 31, 2018 11:58:38 Steven Schveighoffer via 
Digitalmars-d-learn wrote:
 On 1/30/18 8:05 PM, Jonathan M Davis wrote:
 Except that unless front returns by ref, it really doesn't matter
 whether
 front is const unless it's violating the range API, since front is
 supposed to return the same value until popFront is called (or if it's
 assigned a new value via a front that returns by ref). So, in practice,
 putting const on front really doesn't help you any, and it actually
 hurts you for range composability.
Right, but that is the difference between a convention ("front is supposed to...") vs. a compiler-enforced guarantee (modifying data by calling a const-tagged front is a compiler error). If you are OK with conventions, you don't need const at all.
Except that if you're the one writing the function and decided whether it's const or not, you're also the one deciding whether it returns by ref or not. Unless you're dealing with a reference type, and it doesn't return by ref, then const doesn't protect front at all. It just affects whether it can be called on a const range. If you're dealing with generic code, then you have less control, and const starts mattering more, since you don't necessarily know what type is being returned, and if you're returning front from an underlying range, you the choice of eixther returning it by value or returning it by auto ref in case the underlying range returned by ref and passing that refness on is desirable. But const also interacts far more badly with generic code, because the odds are pretty high that it won't work in many cases. So, while in principle, using const to actually have the guarantees is valuable, in practice, it isn't very viable, because D's const is so restrictive. Personally, I avoid const in generic code like the plague, because unless you've restricted the types enough to know what you're dealing with and know that it will work with const, the odds are quite high that you're writing code that's going to fall flat on its face with many types. - Jonathan M Davis
Jan 31
parent Steven Schveighoffer <schveiguy yahoo.com> writes:
On 1/31/18 7:49 PM, Jonathan M Davis wrote:
 On Wednesday, January 31, 2018 11:58:38 Steven Schveighoffer via
 Digitalmars-d-learn wrote:
 On 1/30/18 8:05 PM, Jonathan M Davis wrote:
 Except that unless front returns by ref, it really doesn't matter
 whether
 front is const unless it's violating the range API, since front is
 supposed to return the same value until popFront is called (or if it's
 assigned a new value via a front that returns by ref). So, in practice,
 putting const on front really doesn't help you any, and it actually
 hurts you for range composability.
Right, but that is the difference between a convention ("front is supposed to...") vs. a compiler-enforced guarantee (modifying data by calling a const-tagged front is a compiler error). If you are OK with conventions, you don't need const at all.
Except that if you're the one writing the function and decided whether it's const or not, you're also the one deciding whether it returns by ref or not. Unless you're dealing with a reference type, and it doesn't return by ref, then const doesn't protect front at all. It just affects whether it can be called on a const range.
You are misunderstanding here. You don't put const on front for the purpose of allowing const ranges (which are useless), what it does is say that the compiler guarantees, *even if the range is mutable* that front won't modify it. That is, code like the following is rejected by the compiler: int front() const { return ++val; } In other words, it's a contract that you can read without having to examine the code saying "this won't mutate the range". Sure, you can document "front shouldn't modify the range", and use that convention, but without const, the compiler doesn't care.
 If you're dealing with generic code, then you have less control, and const
 starts mattering more, since you don't necessarily know what type is being
 returned, and if you're returning front from an underlying range, you the
 choice of eixther returning it by value or returning it by auto ref in case
 the underlying range returned by ref and passing that refness on is
 desirable. But const also interacts far more badly with generic code,
 because the odds are pretty high that it won't work in many cases. So, while
 in principle, using const to actually have the guarantees is valuable, in
 practice, it isn't very viable, because D's const is so restrictive.
Technically, wrapping requires introspection. If you don't care about forwarding the "guarantee" of constness, then you can just tag all your functions mutable, but if you do care, then you have to introspect.
 Personally, I avoid const in generic code like the plague, because unless
 you've restricted the types enough to know what you're dealing with and know
 that it will work with const, the odds are quite high that you're writing
 code that's going to fall flat on its face with many types.
Indeed, it's not straightforward, if you have to deal with types that aren't tagged the way they should be. In addition, const is not inferred for templates like other attributes, so you can't rely on that either. -Steve
Jan 31
prev sibling parent reply "H. S. Teoh" <hsteoh quickfur.ath.cx> writes:
On Tue, Jan 30, 2018 at 06:05:47PM -0700, Jonathan M Davis via
Digitalmars-d-learn wrote:
 On Tuesday, January 30, 2018 07:49:28 H. S. Teoh via Digitalmars-d-learn 
 wrote:
[...]
 Simen has had some ideas recently about "head mutable" aka
 tail-const, which could be a first step towards making const ranges,
 or rather tail-const ranges, actually usable:

   https://forum.dlang.org/post/cpxfgdmklgusodqouqdr forum.dlang.org

 tl;dr summary:

 (1) Ranges implement a standard method, tentatively called
     opHeadMutable, that returns a head-mutable version of
     themselves.  For example, const(MyRange!T).opHeadMutable would
     return MyRange!(const(T)).

 (2) Standard library functions would recognize opHeadMutable and use
     it where needed, e.g., when you hand them a const range.

 (3) Profit. :-P
I still need to look over what he's proposing in more detail - it's been proposed before (by Andrei IIRC) that one possible solution would be to add an operator for returning a tail-const version of a type, but no one has ever taken that idea anywhere.
Well, that's essentially what Simen has done, and he has code to show for it.
 Personally, I'm getting to the point that I'd rather just avoid const
 than deal with any further complications for ranges. In principle, I
 like the idea of const, but in practice, it just constantly gets in
 the way, and I've rarely actually seen any benefit from it in either
 C++ or D. I can think of one time in my entire life where const has
 prevented a bug for me - which was when I got the arguments backwards
 to C++'s std::copy function.
I have been saved by const (in D) a few times when I by mistake tried mutating something that shouldn't be mutated. But yeah, more often than not it just gets in the way. However, my hope is that if Simen's proposal gets somewhere, it will reduce the annoyance of const and (hopefully) increase its benefits. Note that while Simen's code example uses ranges, since that's a common blocker for using const, it extends beyond that. For example, consider a const(RefCounted!Object). Right now, this is unusable because you cannot update the reference count of a const object without casting const away and treading into UB territory. But if const(RefCounted!Object).opHeadConst returned a RefCounted!(const(Object)) instead, then this could be made usable: the payload can now become const while keeping the reference count mutable. Of course, as currently designed, the API is kinda awkward. But that's just a syntactic issue. We could call it instead .headConst, and you'd have: // mutable refcount, mutable payload RefCounted!Object // mutable refcount, const payload (useful) RefCounted!Object.headConst --> RefCounted!(const(Object)) // const refcount, const payload (useless) const(RefCounted!Object) Standardizing .headConst means that we now have a reliable way to construct a RefCounted!(const(Object)) from a RefCounted!Object, whereas currently we can only construct const(RefCounted!Object), which is unusable. In general, this lets us construct a Template!(const(T)) from a Template!T without needing to know what Template is. For example, Template could take multiple parameters, like Template!(x,T), such that the correct head-const is actually Template!(x, const(T)). Generic code can't know this, but if .headConst is standardized, then it provides a way for generic code to create a Template!(x, const(T)) from a Template(x,T) without needing special knowledge of Template's implementation. We could even put a generic .headConst in druntime that implements the conversion for built-in types like int* -> const(int)*. Then .headConst becomes the standard idiom to go from any type T to a head-const version of T. Generic code that relies on .headConst would work for both built-in types and custom user types without any change. Best of all, this doesn't even require a language change, which is a big plus.
 At least with immutable, you get implicit sharing and some
 optimization opportunities. In principle, const can get you some of
 the optimization opportunities but only in really restricted
 circumstances or circumstances where you could have used immutable and
 the code would have been the same (e.g. with int).
[...] I haven't thought through it carefully, but if .headConst is a viable solution to the head-const problem, then conceivably we could also extend it to deal with immutable payloads too. Then we could go from, say, RefCounted!(immutable(T)) to RefCounted!(const(T)) generically, without any casting or breaking the type system. We could potentially expand the scope of usefulness of immutable this way, if this approach turns out to be workable. T -- If Java had true garbage collection, most programs would delete themselves upon execution. -- Robert Sewell
Jan 30
next sibling parent reply Simen =?UTF-8?B?S2rDpnLDpXM=?= <simen.kjaras gmail.com> writes:
On Wednesday, 31 January 2018 at 01:45:57 UTC, H. S. Teoh wrote:
 .headConst
.headMutable. :p Head-const is something we generally want to avoid. -- Simen
Jan 30
parent "H. S. Teoh" <hsteoh quickfur.ath.cx> writes:
On Wed, Jan 31, 2018 at 07:08:58AM +0000, Simen Kjærås via Digitalmars-d-learn
wrote:
 On Wednesday, 31 January 2018 at 01:45:57 UTC, H. S. Teoh wrote:
 .headConst
.headMutable. :p Head-const is something we generally want to avoid.
[...] *facepalm* Yes, .headMutable, not .headConst. Argh... T -- VI = Visual Irritation
Jan 31
prev sibling parent reply Simen =?UTF-8?B?S2rDpnLDpXM=?= <simen.kjaras gmail.com> writes:
On Wednesday, 31 January 2018 at 01:45:57 UTC, H. S. Teoh wrote:
 I haven't thought through it carefully, but if .headConst is a 
 viable solution to the head-const problem, then conceivably we 
 could also extend it to deal with immutable payloads too.  Then 
 we could go from, say, RefCounted!(immutable(T)) to 
 RefCounted!(const(T)) generically, without any casting or 
 breaking the type system.  We could potentially expand the 
 scope of usefulness of immutable this way, if this approach 
 turns out to be workable.
Going from RefCounted!(immutable(T)) to RefCounted!(const(T)) is nice, but I'd like to see a conversion from, const(RefCounted!T) to RefCounted!(const(T)). While this cannot be done without casts, the logic can be put inside .headMutable(), and include relevant checks. This will make it much safer than having the programmer cast manually. -- Simen
Jan 31
parent reply "H. S. Teoh" <hsteoh quickfur.ath.cx> writes:
On Thu, Feb 01, 2018 at 07:52:32AM +0000, Simen Kjærås via Digitalmars-d-learn
wrote:
 On Wednesday, 31 January 2018 at 01:45:57 UTC, H. S. Teoh wrote:
 I haven't thought through it carefully, but if .headConst is a
 viable solution to the head-const problem, then conceivably we could
 also extend it to deal with immutable payloads too.  Then we could
 go from, say, RefCounted!(immutable(T)) to RefCounted!(const(T))
 generically, without any casting or breaking the type system.  We
 could potentially expand the scope of usefulness of immutable this
 way, if this approach turns out to be workable.
Going from RefCounted!(immutable(T)) to RefCounted!(const(T)) is nice, but I'd like to see a conversion from, const(RefCounted!T) to RefCounted!(const(T)). While this cannot be done without casts, the logic can be put inside .headMutable(), and include relevant checks. This will make it much safer than having the programmer cast manually.
[...] Hmm. I experimented with this for a bit, and found that if we limit ourselves to head-mutable, then these casts are inescapable. The problem is that if we're handed a const object like const(RefCounted!T), then there's no way we can back out to RefCounted!(const(T)) without casting, because the latter contains a mutable refcount whereas the former, due to const transitivity, must be entirely unmodifiable. If the refcount were stored in the RefCount struct itself, then we could get away with a by-value copy into RefCounted!(const(T)), but unfortunately we can't do that without breaking the refcounting semantics; the refcount must be on the payload itself, and RefCounted is basically just a smart pointer. So const(RefCounted!T), by const transitivity, can only contain a const pointer to the payload, so we're forced to use a cast to get a RefCounted!(const(T)) out of it. And on that note, this casting is NOT safe; for example, if you start with an immutable(RefCounted!T) and implicitly convert it to const(RefCounted!T), then if you cast the latter to RefCounted!(const(T)), you're now violating immutable. And there's no way you can tell from inside .headMutable whether it's safe to cast, because by the time it gets to .headMutable, the original immutable type is already "forgotten". However, if we go back to the idea of tail-const, we could potentially eliminate the need for casts and also avoid breaking immutable. Basically, the problem with writing const(RefCounted!T) is that it's a one-way street: on the scale of increasing restrictiveness, we have the series: RefCounted!T < RefCounted!(const(T)) < const(RefCounted!T) Once you've gone all the way down to const(RefCounted!T), you can no longer safely back out to RefCounted!(const(T)). So that means we want to avoid const(RefCounted!T) completely. Instead, if we standardize on a way to produce a RefCounted!(const(T)) from a RefCounted!T, then we can stop halfway down the one-way street and never need to back up. Let's say we standardize on an operation .tailConst that does these conversions: Container!T.tailConst --> Container!(const(T)) Container!(const(T)).tailConst --> Container!(const(T)) const(Container!T).tailConst --> const(Container!T) // This is allowed because it's possible to implicitly convert // immutable(T) to const(T) internally: Container!(immutable(T)).tailConst --> Container!(const(T)) immutable(Container!T).tailConst --> const(Container!T) where Container can be any template that might want to support tail-const semantics. Essentially, .tailConst becomes the mid-way stand-in for the language's built-in implicit conversions from mutable/immutable to const. So instead of passing around const(Container!T), we'd construct a Container!(const(T)) by calling .tailConst on the original container, and pass that around instead. We can also generalize this via UFCS to built-in reference types: tailConst(T*) --> const(T)* tailConst((const(T))*) --> const(T)* tailConst(const(T*)) --> const(T*) tailConst(immutable(T)*) --> const(T)* tailConst(immutable(T*)) --> const(T*) Then .tailConst becomes a standard way of constructing a tail-const type in the language. No explicit language support is needed. T -- Claiming that your operating system is the best in the world because more people use it is like saying McDonalds makes the best food in the world. -- Carl B. Constantine
Feb 01
parent reply Simen =?UTF-8?B?S2rDpnLDpXM=?= <simen.kjaras gmail.com> writes:
On Thursday, 1 February 2018 at 18:58:15 UTC, H. S. Teoh wrote:
 However, if we go back to the idea of tail-const, we could 
 potentially eliminate the need for casts and also avoid 
 breaking immutable. Basically, the problem with writing 
 const(RefCounted!T) is that it's a one-way street: on the scale 
 of increasing restrictiveness, we have the series:

 	RefCounted!T < RefCounted!(const(T)) < const(RefCounted!T)

 Once you've gone all the way down to const(RefCounted!T), you 
 can no
 longer safely back out to RefCounted!(const(T)).

 So that means we want to avoid const(RefCounted!T) completely.
I'm not really saying I disagree with that, but it's just not realistic. Which code would you rather write? void foo(T)(const T t) {} foo(myValue); or: void foo(T)(T t) if (isTailConst!T) {} foo(myValue.tailConst); The beauty of .headMutable is it generally doesn't affect user code. If we have to tell people not to use const(T) because its semantics are broken, we've failed. Now, if at any point in your program you have an immutable(RefCounted!T), something's gone horribly wrong - basically all of the RefCounted's semantics break down when it's immutable, and any attempt at fixing it is undefined behavior. I think we can safely disregard the problems of immutable(RefCounted!T). Once we've defined immutable(RefCounted!T) to be undefined behavior, suddenly casting from const(RefCounted!T) to RefCounted!(const(T)) is OK again. -- Simen
Feb 01
next sibling parent reply "H. S. Teoh" <hsteoh quickfur.ath.cx> writes:
On Fri, Feb 02, 2018 at 07:06:56AM +0000, Simen Kjærås via Digitalmars-d-learn
wrote:
 On Thursday, 1 February 2018 at 18:58:15 UTC, H. S. Teoh wrote:
 However, if we go back to the idea of tail-const, we could
 potentially eliminate the need for casts and also avoid breaking
 immutable.  Basically, the problem with writing const(RefCounted!T)
 is that it's a one-way street: on the scale of increasing
 restrictiveness, we have the series:
 
 	RefCounted!T < RefCounted!(const(T)) < const(RefCounted!T)
 
 Once you've gone all the way down to const(RefCounted!T), you can no
 longer safely back out to RefCounted!(const(T)).
 
 So that means we want to avoid const(RefCounted!T) completely.
I'm not really saying I disagree with that, but it's just not realistic. Which code would you rather write? void foo(T)(const T t) {} foo(myValue); or: void foo(T)(T t) if (isTailConst!T) {} foo(myValue.tailConst); The beauty of .headMutable is it generally doesn't affect user code. If we have to tell people not to use const(T) because its semantics are broken, we've failed.
Its semantics are not broken; it's just harder to use. Due to const transitivity, it's an all-or-nothing deal. .tailConst gives us the middle ground.
 Now, if at any point in your program you have an
 immutable(RefCounted!T), something's gone horribly wrong - basically
 all of the RefCounted's semantics break down when it's immutable, and
 any attempt at fixing it is undefined behavior. I think we can safely
 disregard the problems of immutable(RefCounted!T).
Immutable may be useless for RefCounted, but I'm thinking of containers and wrapper types in general. Today, due to ranges being basically useless when you can't mutate them, you're forced to choose between a completely mutable range, or no range at all. Having .tailConst as a standard construction lets you create a usable range that provides a compiler-verified guarantee that nobody will be able to modify range elements. Today we don't have a standard way of doing this, and so people have given up on using const with ranges. We're missing out on the guarantees that const provides. .tailConst lets us get some of those guarantees back without requiring us to tie our hands behind our backs.
 Once we've defined immutable(RefCounted!T) to be undefined behavior,
 suddenly casting from const(RefCounted!T) to RefCounted!(const(T)) is
 OK again.
[...] The problem with this is that we're now relying on convention rather than something that can be statically verified by the compiler. Once you allow casting away const, there's no longer a guarantee that somebody didn't pass in an immutable, whether by mistake or otherwise. We know from C/C++ where programming by convention leads us. :-P T -- Having a smoking section in a restaurant is like having a peeing section in a swimming pool. -- Edward Burr
Feb 02
parent Simen =?UTF-8?B?S2rDpnLDpXM=?= <simen.kjaras gmail.com> writes:
On Friday, 2 February 2018 at 14:29:34 UTC, H. S. Teoh wrote:
 Its semantics are not broken; it's just harder to use. Due to 
 const transitivity, it's an all-or-nothing deal.  .tailConst 
 gives us the middle ground.
If the semantics of const means that users will have to write .tailConst all over the place, it's broken. If it means that users can't use const(T) because they actually want TailConst!T, it's broken. TailConst seems like the logical solution to the problem, but it isn't. It directly impacts user code and it leads to lots of boilerplate. In addition to .tailConst, we also need .tailImmutable. And to top it off, it just doesn't mix with const at all - if you pass it as a const parameter, it's broken. If it's part of a struct or class with const methods, it's broken. It infects every part of your codebase that touches it, it forces you to basically implement your own const system in templates, and it makes const even harder to use than it currently is. Tail-const is the more intuitive way to think of it, so if I'm wrong, please show me.
 Once we've defined immutable(RefCounted!T) to be undefined 
 behavior,
 suddenly casting from const(RefCounted!T) to 
 RefCounted!(const(T)) is
 OK again.
[...] The problem with this is that we're now relying on convention rather than something that can be statically verified by the compiler. Once you allow casting away const, there's no longer a guarantee that somebody didn't pass in an immutable, whether by mistake or otherwise. We know from C/C++ where programming by convention leads us. :-P
True. Sadly, there's no way to tell the type system 'this type should never be immutable'. Maybe such a thing should be in the language. Meanwhile, if RefCounted!T implements .headMutable, it can check at runtime that the refcount is in writable memory.
 Though the above currently doesn't compile, it seems because 
 the compiler doesn't know how to resolve Wrapper!(const(T)) 
 given a Wrapper!T despite the alias this.
Yeah, I found the same bug when playing with .headMutable: https://issues.dlang.org/show_bug.cgi?id=18260 -- Simen
Feb 04
prev sibling parent "H. S. Teoh" <hsteoh quickfur.ath.cx> writes:
On Fri, Feb 02, 2018 at 07:06:56AM +0000, Simen Kjærås via Digitalmars-d-learn
wrote:
[...]
 Which code would you rather write?
 
 void foo(T)(const T t) {}
 foo(myValue);
 
 or:
 
 void foo(T)(T t) if (isTailConst!T) {}
 foo(myValue.tailConst);
[...] More thoughts on this: what if we made it so that Wrapper!T is implicitly convertible to Wrapper!(const T)? Something like this: auto foo(Wrapper,T)(Wrapper!(const(T)) t) ... struct Wrapper(T) { auto tailConst() { return Wrapper!(const(T))(...); } alias tailConst this; } void main() { Wrapper!int w; foo(w); } It will interact badly if Wrapper is already using alias this for something else, but this lets us keep the convenience of implicitly decaying to tail-const without requiring an explicit call to .tailConst. Though the above currently doesn't compile, it seems because the compiler doesn't know how to resolve Wrapper!(const(T)) given a Wrapper!T despite the alias this. However, removing `Wrapper` from the template arguments of foo() does work; and seems to have the right semantics. Seems to be just a limitation of IFTI. So calling a function that expects, say, RefCounted!(const T), with an argument of type RefCounted!T can already be made to work today, if we tie .tailConst to alias this. The fully general solution will have to wait until we can improve IFTI to support the generic case where the wrapper type is also a template parameter. Might be worth filing an enhancement against the compiler to support this? If this can be made to work, we may not even need a standard name for .tailConst, as long as the wrapper type can somehow make itself decay into its tail-const version implicitly. T -- In theory, there is no difference between theory and practice.
Feb 02