April 30, 2017
On 04/27/2017 07:12 PM, Moritz Maxeiner wrote:
> On Thursday, 27 April 2017 at 20:04:32 UTC, Stanislav Blinov wrote:
>> On Thursday, 27 April 2017 at 19:57:52 UTC, Andrei Alexandrescu wrote:
>>> https://github.com/dlang/phobos/pull/5355
>>>
>>> Andrei
>>
>> And then we'd probably need INoGCAllocator and ISharedNOGCAllocator...
>
> Wasn't one major selling point of compile time introspection / duck
> typing that we could stop using interfaces such... naming schemes?

The allocators design is interesting in that it has a full DbI core, on top of which resides a thin dynamically-type interface (IAllocator and ISharedAllocator). We're still exploring the idioms enabled by this interaction. -- Andrei
April 30, 2017
On 04/27/2017 07:35 PM, Stanislav Blinov wrote:
> IAllocator is too high level an interface, it doesn't carry any
> information as to what type of memory it can allocate (so we can only
> assume unshared), and does or does it not use GC (so we can only assume
> GC).

Initially all fresh memory is unshared. Whether or not the user subsequently shares it is of no consequence to the allocator. Allocators must, however, distinguish between reentrant and non-reentrant calls to the allocation primitives.

With regard to whether the memory is collectable or not we need some more thinking.

> If we are to devise types with allocators as members instead of type
> arguments, we need the additional information. Better to catch invalid
> assignment at compile time than to chase down how a stack allocator from
> one thread ended up in another.

A pass through the root allocators (Mallocator, GCAllocator etc) figuring out what attributes could be meaningfully attached would be welcome. The rest would rely on inference.


Thanks,

Andrei

May 01, 2017
On Sunday, 30 April 2017 at 21:43:26 UTC, Andrei Alexandrescu wrote:
> On 04/27/2017 07:35 PM, Stanislav Blinov wrote:
>> IAllocator is too high level an interface, it doesn't carry any
>> information as to what type of memory it can allocate (so we can only
>> assume unshared), and does or does it not use GC (so we can only assume
>> GC).
>
> Initially all fresh memory is unshared. Whether or not the user subsequently shares it is of no consequence to the allocator.

Why would we need any ISharedAllocator then? If we're to force users to rely on *documentation* whether or not they can cast allocated memory to shared, there is no point in static checks, they'll only get in the way of user's attempts to try and stitch together the pieces. They're on their own anyway at that point.

>> If we are to devise types with allocators as members instead of type
>> arguments, we need the additional information. Better to catch invalid
>> assignment at compile time than to chase down how a stack allocator from
>> one thread ended up in another.
>
> A pass through the root allocators (Mallocator, GCAllocator etc) figuring out what attributes could be meaningfully attached would be welcome. The rest would rely on inference.

If we're type-erasing allocators, we have to have the ability to statically specify what traits we're interested in (i.e. *don't* want to erase). Otherwise, inference will not be of any help.
Consider an example, inspired by the discussion of Atila's automem library:

import std.experimental.allocator;
import std.algorithm.mutation : move;

// Allocator is not a type parameter
struct SmartPtr(T)
{
    // we can infer all we need form A...
    this(A, Args...)(A alloc, auto ref Args args)
    if (isAllocatorImpl!A)
    {
        // ...but we immediately throw the inference away:
        allocator_ = alloc;
        block_ = cast(Block[])allocator_.allocate(Block.sizeof);
        // SmartPtr logic snipped...
    }

    ~this()
    {
        // ...SmartPtr logic snipped
        allocator_.deallocate(block_);
    }

private:
    struct Block
    {
        size_t refCount;
        void[T.sizeof] payload;
    }

    Block[] block_;
    IAllocator allocator_;
}

struct Data {
    ~this() @nogc { /*...*/ }
    // the rest of implementation is @nogc as well
}

struct MyType
{
    // won't compile: IAllocator's methods aren't @nogc,
    // so SmartPtr's dtor isn't either
    this(SmartPtr!Data data) @nogc
    {
        data_ = move(data);
    }

private:
    SmartPtr!Data data_;
}

Obviously, IAllocator is not the tool for the job here. This calls for something like this:

enum AllocTraits
{
    none  = 0x00,
    nogc  = 0x01,
    share = 0x02
}

alias AllocatorInterface(AllocTraits) = // ???

struct SmartPtr(T, AllocTraits traits = AllocTraits.none)
{
    this(A, Args...)(A alloc, auto ref Args args)
    {
        // this should not compile if A isn't compatible
        // with `traits`:
        allocator_ = alloc;
        block_ = allocator_.allocate(T.sizeof);
        // SmartPtr logic snipped...
    }

    ~this()
    {
        // ...SmartPtr logic snipped
        allocator_.deallocate(block_);
    }

private:
    void[] block_;
    AllocatorInterface!AllocTraits allocator_;
}

alias DataPtr = SmartPtr!(Data, AllocTraits.nogc);

struct MyType
{
    this(DataPtr data) @nogc
    {
        data_ = move(data);
    }

private:
    DataPtr data_;
}

That *would* be able to rely on inference. Question is, what is AllocatorInterface? Should we push such type erasure to the users?
May 01, 2017
On Monday, 1 May 2017 at 00:43:22 UTC, Stanislav Blinov wrote:

>         block_ = allocator_.allocate(T.sizeof);

Obviously, should be Block.sizeof, and

>     AllocatorInterface!AllocTraits allocator_;

should be AllocatorInterface!traits allocator_
May 01, 2017
On 4/30/17 8:43 PM, Stanislav Blinov wrote:
> On Sunday, 30 April 2017 at 21:43:26 UTC, Andrei Alexandrescu wrote:
>> On 04/27/2017 07:35 PM, Stanislav Blinov wrote:
>>> IAllocator is too high level an interface, it doesn't carry any
>>> information as to what type of memory it can allocate (so we can only
>>> assume unshared), and does or does it not use GC (so we can only assume
>>> GC).
>>
>> Initially all fresh memory is unshared. Whether or not the user subsequently shares it is of no consequence to the allocator.
> 
> Why would we need any ISharedAllocator then?

The allocator itself may be shared across threads, in which case its primitives need to be reentrant. -- Andrei
May 01, 2017
On Friday, 28 April 2017 at 22:18:54 UTC, Atila Neves wrote:
> Done. I also added to the README that it has its own versions of the range constraints from Phobos that can be used with `@models`.
>
> Atila

Example of an error message in the README would be great too.
May 01, 2017
On Monday, 1 May 2017 at 04:54:28 UTC, Andrei Alexandrescu wrote:
> On 4/30/17 8:43 PM, Stanislav Blinov wrote:
>> On Sunday, 30 April 2017 at 21:43:26 UTC, Andrei Alexandrescu wrote:
>>> On 04/27/2017 07:35 PM, Stanislav Blinov wrote:
>>>> IAllocator is too high level an interface, it doesn't carry any
>>>> information as to what type of memory it can allocate (so we can only
>>>> assume unshared), and does or does it not use GC (so we can only assume
>>>> GC).
>>>
>>> Initially all fresh memory is unshared. Whether or not the user subsequently shares it is of no consequence to the allocator.
>> 
>> Why would we need any ISharedAllocator then?
>
> The allocator itself may be shared across threads...

But it is no different in case of memory. Allocator that allocates shared memory should either return shared(void)[] or have an explicit interface (allocateShared et al.), not just have the  "you may cast result of allocate() to shared" somewhere in the docs. We should let the language do the work it can, not send us to RTFM. Hence my earlier statement that more interfaces are needed, as one possible (albeit not pretty) solution.

May 01, 2017
On Sunday, 30 April 2017 at 21:43:26 UTC, Andrei Alexandrescu wrote:
>
> A pass through the root allocators (Mallocator, GCAllocator etc) figuring out what attributes could be meaningfully attached would be welcome. The rest would rely on inference.
>
>
> Thanks,
>
> Andrei

IAllocator being fully @nogc would be a comforting guarantee, as runtime dispatch makes for lighter types.
May 01, 2017
On 05/01/2017 08:12 AM, Guillaume Piolat wrote:
> On Sunday, 30 April 2017 at 21:43:26 UTC, Andrei Alexandrescu wrote:
>>
>> A pass through the root allocators (Mallocator, GCAllocator etc)
>> figuring out what attributes could be meaningfully attached would be
>> welcome. The rest would rely on inference.
>>
>>
>> Thanks,
>>
>> Andrei
>
> IAllocator being fully @nogc would be a comforting guarantee, as runtime
> dispatch makes for lighter types.

As I said (and am encouraging you to do this), this is achieved through simple variance:

interface INoGCAllocator : IAllocator {
   ... override all methods here as @nogc ...
}

This is possible because a @nogc method may override one that is not @nogc. @nogc being more restrictive it is contravariant with no-@nogc.

Also IAllocator should have a few @nogc methods to start with; there's no reason e.g. for empty() to create garbage.

Could you please initiate a PR?


Andrei

May 01, 2017
On 04/30/2017 05:39 PM, Andrei Alexandrescu wrote:
> The allocators design is interesting in that it has a full DbI core, on
> top of which resides a thin dynamically-type interface (IAllocator and
> ISharedAllocator). We're still exploring the idioms enabled by this
> interaction. -- Andrei

I assume this dual-interface design (DbI + OO) is, much like std.digest, a way to have our template/DbI cake and still permit things to be selected at runtime too (via OO), right?

If so, this is a topic that's been on my mind lately, and I'd like to offer some thoughts I've had (well, some partial thoughts anyway):

Part A: While I like that the std.digest design allows both DbI and runtime-selection of arbitrary user-created types, it has a few issues:

1. It feels awkward that it needs essentially duplicate APIs under two different paradigms to pull it off.

2. If a library (such as mine) wants to support both versions, then it needs a little bit of extra plumbing to accommodate that. Or at least it seemed to in my case. (std.digest itself even has a little bit of extra plumbing because of it: WrapperDigest)

3. The OO version, being OO, does involve some type erasure.

Now, fast-forward to:

Part B: On a separate project, I've used Sonke's TaggedAlgebraic before <https://github.com/s-ludwig/taggedalgebraic>. It's kind of interesting: It's much like Phobos's Algebraic, except it allows function/member forwarding to the current underlying value *without* having to explicitly check what the current value's type is (Don't recall offhand exactly how it handles members that don't exist on the current type, probably throws I guess, although I'd imagine it would be possible to statically disallow access to members known at compile-time to not exist in *any* of the possible underlying types. But I digress).

But then:

Part C: At the time, I saw TaggedAlgebraic's member-forwarding as a minor little convenience feature. But the other day I was working with some code that used and built upon std.digest, and something occurred to me:

If we had a type similar to TaggedAlgebraic, but was an open variant rather than a closed algebraic (Ie, a type like Phobos's Variant, but forwarded member access without first requiring explicit conversion to the exact underlying type), then *that* could be used to permit runtime-selection between arbitrary DbI types *without* the type-erasure and "duality" of adding in OO (or at least, less type-erasure anyway). Just maybe need the ability to filter the allowable types via a constraint (such as isBidirctionalRange, etc), and then, IIUC it should be possible for the variant-like supertype to qualify as an honest-to-goodness instance of the DbI interface. Voila: OO has been obsoleted.

Destroy?