Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

[PC] Simulating Monads, Lambdas, Closures, and Laziness

121 views
Skip to first unread message

M Joshua Ryan

unread,
Apr 1, 2019, 5:27:12 PM4/1/19
to
Norah is my computer's name.


#include <ctype.h>
#include <stdlib.h>
#include <stdio.h>

typedef union object *object;
typedef enum { INT, LIST, SUSP, PARSER, OPER } tag;
union object { tag t;
struct { tag t; int i; } Int;
struct { tag t; object a, b; } List;
struct { tag t; void *v; object (*f)( void * ); } Susp;
struct { tag t; void *v; object (*f)( void *, object ); } Parser;
struct { tag t; void *v; object (*f)( void *, object ); } Oper;
};

object new_( union object o ){
object p = calloc( 1, sizeof o );
return p ? *p = o, p : 0;
}
#define OBJECT(...) new_( (union object){ __VA_ARGS__ } )

object Int( int i ){ return OBJECT( .Int = { INT, i } ); }
object one( object a ){ return OBJECT( .List = { LIST, a, NULL } ); }
object cons( object a, object b ){ return OBJECT( .List = { LIST, a, b } ); }

object Susp( void *v, object (*f)( void * ) ){ return OBJECT( .Susp = { SUSP, v, f } ); }
object Parser( void *v, object (*f)( void *, object ) ){ return OBJECT( .Parser = { PARSER, v, f } ); }
object Oper( void *v, object (*f)( void *, object ) ){ return OBJECT( .Oper = { OPER, v, f } ); }

object at( object a ){ return a && a->t == SUSP ? at( a->Susp.f( a->Susp.v ) ) : a; }
object x( object a ){ return a && a->t == LIST ? a->List.a : NULL; }
object xs( object a ){ return a && a->t == LIST ? a->List.b = at( a->List.b ) : NULL; }

object parse( object p, object input ){
return p->t == PARSER ? p->Parser.f( p->Parser.v, input ) : NULL;
}


object string_input( void *v ){
char *p = v;
return *p ? cons( Int( *p ), Susp( p+1, string_input ) ) : NULL;
}


object fresult( void *v, object input ){
return one( cons( v, input ) );
}
object result( object v ){
return Parser( v, fresult );
}


object fitem( void *v, object input ){
return input ? one( cons( x( input ), xs( input ) ) ) : NULL;
}
object item( void ){
return Parser( 0, fitem );
}


object apply( object f, object o ){
return f->t == OPER ? f->Oper.f( f->Oper.v, o ) : NULL;
}
object map( object f, object o ){
return o ? cons( apply( f, x( o ) ), map( f, xs( o ) ) ) : NULL;
}
object last( object a ){
return a->List.b ? last( a->List.b ) : a;
}
object append( object a, object b ){
return a ? last( a )->List.b = b, a : b;
}
object join( object o ){
return o ? append( x( o ), join( xs( o ) ) ) : NULL;
}
object fbind( void *v, object input ){
object o = v;
object p = x( o );
object f = xs( o );
return join( map( f, parse( p, input ) ) );
}
object bind( object p, object f ){
return Parser( cons( p, f ), fbind );
}


int alpha( object o ){
return isalpha( o->Int.i );
}
int digit( object o ){
return isdigit( o->Int.i );
}
object fsat( void *v, object o ){
int (*pred)( object ) = v;
return pred( x( o ) ) ? one( o ) : NULL;
}
object sat( int (*pred)( object ) ){
return bind( item(), Oper( pred, fsat ) );
}
object is_alpha( void ){
return sat( alpha );
}
object is_digit( void ){
return sat( digit );
}


object seq( object p, object q ){
}

void print( object o ){
if( !o ){ printf( "() " ); return; }
switch( o->t ){
case INT: printf( "%d ", o->Int.i ); break;
case LIST: printf( "(" ); print( o->List.a ); print( o->List.b ); printf( ") " ); break;
case SUSP: printf( "SUSP " ); break;
default: break;
}
}

int main(){
char *s = "my string";
object p = string_input( s );
printf( "%c", x( p )->Int.i );
printf( "%c", x( xs( p ) )->Int.i );
printf( "%c", x( xs( xs( p ) ) )->Int.i );
puts("");
print( p ), puts("");
object q = result( Int(42) );
print( parse( q, p ) ), puts("");
print( parse( item(), p ) ), puts("");
print( parse( is_alpha(), p ) ), puts("");
print( parse( is_digit(), p ) ), puts("");
}



]0;~/pcomb
Norah@laptop ~/pcomb
$ !m
make CFLAGS=-g pc7
cc -g pc7.c -o pc7
]0;~/pcomb
Norah@laptop ~/pcomb
$ ./pc7
my
(109 (121 (32 SUSP ) ) )
((42 (109 (121 (32 SUSP ) ) ) ) () )
((109 (121 (32 SUSP ) ) ) () )
((109 (121 (32 SUSP ) ) ) () )
()
]0;~/pcomb
Norah@laptop ~/pcomb
$

Chad

unread,
Apr 1, 2019, 5:43:10 PM4/1/19
to
And what happens if I take the factorial of 1024 on a 32 bit machine using a 600 Mhz Processor? Because *both* python and Haskell can handle such a scenario quickly and correctly.

luser droog

unread,
Apr 1, 2019, 5:57:51 PM4/1/19
to
On Monday, April 1, 2019 at 4:43:10 PM UTC-5, Chad wrote:
> On Monday, April 1, 2019 at 2:27:12 PM UTC-7, luser droog wrote:
> > Subject: [PC] Simulating Monads, Lambdas, Closures, and Laziness
> > Norah is my computer's name.
> >
> >
<snip>
> And what happens if I take the factorial of 1024 on a 32 bit machine using a 600 Mhz Processor? Because *both* python and Haskell can handle such a scenario quickly and correctly.

This is not a bignum implementation, although conceivably that could be done
on top of it.

Steering toward what the code actually does, laziness is accomplished with
a function which yields a list containing a suspension. A suspension is
implemented as a closure which is implemented as an object containing
a function pointer and a context pointer.

The function 'string_input()' converts a string into a lazy list of characters.
The suspended closure is a recursive reference to 'string_input' itself.

Ben Bacarisse

unread,
Apr 1, 2019, 6:55:40 PM4/1/19
to
This is interesting, and I admire your bravery in wading into these
shark-infested waters dressed only in C, so I hope you won't find these
remarks overly negative.

First, I don't see any closures because I don't see any environments. I
don't think that's a problem -- you may not need them -- but it affects
the description and that affects the readers expectations.

Secondly, a monad is very general. I see bind and return functions but
they seem to be tied to parsers. Am I to read bind and return as a
specific instance of a monad?

Some comments on the code...

M Joshua Ryan <luser...@gmail.com> writes:
> Norah is my computer's name.
>
>
> #include <ctype.h>
> #include <stdlib.h>
> #include <stdio.h>
>
> typedef union object *object;

I'm not a fan of hiding a pointer like this. It tripped me up for a
while. For the price of a space and a * you lose the ability to remind
the reader that it's pointers all the way down. However, I'm used to
this construct, so I got over my initial stumble pretty quickly. It's a
small point.

> typedef enum { INT, LIST, SUSP, PARSER, OPER } tag;
> union object { tag t;
> struct { tag t; int i; } Int;
> struct { tag t; object a, b; } List;
> struct { tag t; void *v; object (*f)( void * ); } Susp;
> struct { tag t; void *v; object (*f)( void *, object ); } Parser;
> struct { tag t; void *v; object (*f)( void *, object ); } Oper;
> };

I would be tempted to make all of these last three take the same
function pointer type (giving the suspension a dummy object), and to
typedef that function (but not the pointer type!) so as to tidy all this
up a bit.

<cut constructors>

> object at( object a ){ return a && a->t == SUSP ? at( a->Susp.f( a->Susp.v ) ) : a; }
> object x( object a ){ return a && a->t == LIST ? a->List.a : NULL; }

Why is there no at( a->List.a ) here? Do you know there is never a
suspension here? Is that a safe assumption in general? (I really don't
know -- just asking.)

> object xs( object a ){ return a && a->t == LIST ? a->List.b = at( a->List.b ) : NULL; }

You are cleverly doing a sort of pauper's graph reduction with that
assignment. It's a neat idea (in a sneaky sort of way), but I am used
to the suspension being evaluated when a value is actually used. I hope
you don't find this is ever one step too soon. If you did delay until
actual last-minute use, you would not be able to replace the list node,
so I see why you are doing it like this.

> object parse( object p, object input ){
> return p->t == PARSER ? p->Parser.f( p->Parser.v, input ) : NULL;
> }

I am out of time but for one last remark, and I can make it here though
it applies to other functions too. You might find it more helpful, down
the line, to replace the

return check_type ? something : NULL;

pattern with

retutn check_type_or_fail(), something;

In code like this, NULL is a perfectly fine value (it's like Lisp's nil)
and, with all the checks for it, your code will usually just elegantly,
and silently, stop processing when it pops up. That's been my
experience at least...

<cut more code I don't have time for>
--
Ben.

luser droog

unread,
Apr 2, 2019, 3:31:27 AM4/2/19
to
On Monday, April 1, 2019 at 5:55:40 PM UTC-5, Ben Bacarisse wrote:
> This is interesting, and I admire your bravery in wading into these
> shark-infested waters dressed only in C, so I hope you won't find these
> remarks overly negative.

I hear you. I knew the code wasn't perfect (I had only just gotten it to
work).

> First, I don't see any closures because I don't see any environments. I
> don't think that's a problem -- you may not need them -- but it affects
> the description and that affects the readers expectations.

That's a good point. I was following Hutton and Meijer as mentioned in a
previous thread. Their implementation of 'result' uses a closure, so that
was the mental association I had while researching and experimenting.
So I didn't even notice when I disappeared. I think it got lost in the
translation into PostScript. I got the PS version to work by dynamically
generating a procedure, and by doing that I was able to "hard patch" the
value directly into the code. And so there was no longer any need for an
environment (or variables).

When moving to C, I only needed the single value to be accessible to the
function and associated with that instance of the function. I briefly
entertained the idea of making association lists, and if a future function
needs more than one "closed" value then it's still an option. That might
make it more recognizable as a closure.

There's also a sort of "double indirection" in the code due to my process
of moving ideas from the FP paradigm to OO and then doing "fake" OOP in C.

> Secondly, a monad is very general. I see bind and return functions but
> they seem to be tied to parsers. Am I to read bind and return as a
> specific instance of a monad?

Yes. 'result' and 'bind' provide monadic behavior (just) for parsers. I think
there's also the makings of a list monad in there, too.

So, yes, there's not actually a general Monad that can be instantiated
with different types. And there's no closures. And of course, there never
was a lambda. I think the laziness is pretty real, though. I got ideas
from the classic papers.

https://www2.seas.gwu.edu/~rhyspj/spring11cs3221/lab8/henderson.pdf
http://www.cs.indiana.edu/pub/techreports/TR44.pdf
http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.40.237&rep=rep1&type=pdf

> Some comments on the code...
>
> M Joshua Ryan <luser...@gmail.com> writes:
> > Norah is my computer's name.
> >
> >
> > #include <ctype.h>
> > #include <stdlib.h>
> > #include <stdio.h>
> >
> > typedef union object *object;
>
> I'm not a fan of hiding a pointer like this. It tripped me up for a
> while. For the price of a space and a * you lose the ability to remind
> the reader that it's pointers all the way down. However, I'm used to
> this construct, so I got over my initial stumble pretty quickly. It's a
> small point.

Point taken. I keep on doing this. And it keeps creating (some) difficulty
for others. I really want to abstract away from the pointerness. Would it
help to make it "louder"? sthg like:

#define HIDDEN_POINTER_TO *
typedef union object HIDDEN_POINTER_TO object;


> > typedef enum { INT, LIST, SUSP, PARSER, OPER } tag;
> > union object { tag t;
> > struct { tag t; int i; } Int;
> > struct { tag t; object a, b; } List;
> > struct { tag t; void *v; object (*f)( void * ); } Susp;
> > struct { tag t; void *v; object (*f)( void *, object ); } Parser;
> > struct { tag t; void *v; object (*f)( void *, object ); } Oper;
> > };
>
> I would be tempted to make all of these last three take the same
> function pointer type (giving the suspension a dummy object), and to
> typedef that function (but not the pointer type!) so as to tidy all this
> up a bit.

That's definitely worth considering. An earlier (failed) draft had more
typedefs for functions (well, the pointer type, as you might have guessed).
But it got confusing to me. I gave them bad names and went down a fruitless
path of naming the functions after their signatures. So I had FZ for
suspensions ('Z'ero arguments), FO for operators (one 'O'bject argument),
and FOO for parsers.


> <cut constructors>
>
> > object at( object a ){ return a && a->t == SUSP ? at( a->Susp.f( a->Susp.v ) ) : a; }
> > object x( object a ){ return a && a->t == LIST ? a->List.a : NULL; }
>
> Why is there no at( a->List.a ) here? Do you know there is never a
> suspension here? Is that a safe assumption in general? (I really don't
> know -- just asking.)

In the PostScript version there never ended up being any suspensions
in the "car" of the list, but it probably would be good to handle it
to make the code more generally useful.

> > object xs( object a ){ return a && a->t == LIST ? a->List.b = at( a->List.b ) : NULL; }
>
> You are cleverly doing a sort of pauper's graph reduction with that
> assignment. It's a neat idea (in a sneaky sort of way), but I am used
> to the suspension being evaluated when a value is actually used. I hope
> you don't find this is ever one step too soon. If you did delay until
> actual last-minute use, you would not be able to replace the list node,
> so I see why you are doing it like this.

That also came from the PS code. Since all list access goes through the x() and
xs() functions, it was a convenient place to do the update. I have considered
adding comments to the parts where mutations happen. ///MUTATION!
append() and join() would then needs such comments, but I think bind() might
not since the mutations done by join() are entirely /internal/ in bind().

> > object parse( object p, object input ){
> > return p->t == PARSER ? p->Parser.f( p->Parser.v, input ) : NULL;
> > }
>
> I am out of time but for one last remark, and I can make it here though
> it applies to other functions too. You might find it more helpful, down
> the line, to replace the
>
> return check_type ? something : NULL;
>
> pattern with
>
> retutn check_type_or_fail(), something;

Do you mean '&&' instead of ','?

> In code like this, NULL is a perfectly fine value (it's like Lisp's nil)
> and, with all the checks for it, your code will usually just elegantly,
> and silently, stop processing when it pops up. That's been my
> experience at least...
>
> <cut more code I don't have time for>

Much obliged. I had intend to write a lengthy description and explain
some of the pieces in detail. But ... writing is hard. I was excited that
the thing actually worked.

I noticed in another thread, a question about seq() came up. I thought
it might be necessary to write this function in order to get bind()
to work. As the name might suggest, it performs a sequence of two
parsers. I did get it written shortly after posting.


object fprepend( void *v, object o ){
return cons( cons( v, x( o ) ), xs( o ) );
}
object prepend( object a, object b ){
return map( Oper( a, fprepend ), b );
}
object fseq( void *v, object o ){
object q = v;
return prepend( x( o ), parse( q, xs( o ) ) );
}
object seq( object p, object q ){
return bind( p, Oper( q, fseq ) );
}


// ... in main() ...
object r = seq( is_alpha(), is_alpha() );
print( parse( r, p ) ), puts("");
//}

The next exercise will be a regex parser which produces
a parser as a result.

David Brown

unread,
Apr 2, 2019, 4:25:19 AM4/2/19
to
On 02/04/2019 09:31, luser droog wrote:
> On Monday, April 1, 2019 at 5:55:40 PM UTC-5, Ben Bacarisse wrote:
>>>
>>> typedef union object *object;
>>
>> I'm not a fan of hiding a pointer like this. It tripped me up for a
>> while. For the price of a space and a * you lose the ability to remind
>> the reader that it's pointers all the way down. However, I'm used to
>> this construct, so I got over my initial stumble pretty quickly. It's a
>> small point.
>
> Point taken. I keep on doing this. And it keeps creating (some) difficulty
> for others. I really want to abstract away from the pointerness. Would it
> help to make it "louder"? sthg like:
>
> #define HIDDEN_POINTER_TO *
> typedef union object HIDDEN_POINTER_TO object;
>
>

A very simple solution is to give the typedef name something that
indicates it is a pointer:

typedef union object * pObject;

It is quite common to see types that are pointers start with a "p", or
function pointer types that start with an "f". Some people also use "s"
for structs, and "u" for unions. I am not a fan of going overboard with
this sort of thing - you end up with "Systems Hungarian notation" (using
Wikipedia's term here), which is ugly, unhelpful, and scales extremely
badly. But I often have a "p" on pointer types.

Obviously you can also have suffixes - "object_ptr", or whatever you
like here. But if it is clear to the reader that this is a pointer
type, it should be fine.

The one thing I would /never/ do, however, is use a typedef of a "union
X" or "struct X" to a /pointer/ to an X. "typedef union X X;" is fine,
IMHO, as is "typedef struct Y Y;". I personally prefer to do that, then
use X or Y directly (maybe it is C++ influence creeping in) - other
people specifically dislike it. It is your choice there. But if you
have a struct or union tag X, and a typedef X, then I strongly recommend
that these represent exactly the same type.

Whether you fix that by changing the name of the underlying union type,
or by changing the name of the typedef, will depend on how you want to
use them later in the code. If the union part is to be considered a
hidden internal structure, and everything in user code should use the
pointer type, then maybe you want:

typedef union object_internal * object;

Ben Bacarisse

unread,
Apr 2, 2019, 7:10:49 AM4/2/19
to
luser droog <luser...@gmail.com> writes:

> On Monday, April 1, 2019 at 5:55:40 PM UTC-5, Ben Bacarisse wrote:
>> M Joshua Ryan <luser...@gmail.com> writes:
<cut>
>> > object at( object a ){ return a && a->t == SUSP ? at( a->Susp.f( a->Susp.v ) ) : a; }
>> > object x( object a ){ return a && a->t == LIST ? a->List.a : NULL; }
<cut>
>> > object xs( object a ){ return a && a->t == LIST ? a->List.b = at( a->List.b ) : NULL; }
>>
>> You are cleverly doing a sort of pauper's graph reduction with that
>> assignment. It's a neat idea (in a sneaky sort of way), but I am used
>> to the suspension being evaluated when a value is actually used. I hope
>> you don't find this is ever one step too soon. If you did delay until
>> actual last-minute use, you would not be able to replace the list node,
>> so I see why you are doing it like this.
>
> That also came from the PS code. Since all list access goes through the x() and
> xs() functions, it was a convenient place to do the update.

If everything were "lazy", you could build a new list from the head and
tail of another, with no suspensions being evaluated. You can't do this
if all list access goes through xs.

Thinking about this a bit more, you have made the tail position of a
list rather special. Not only is the head never a suspension, but
(unless I've missed something else) the list itself can't be. In
general, both head(a) and tail(a) would be valid on a SUSP object as
well as a LIST object, and tail(a) on a SUSP would evaluate the
suspension to get a list node and return the tail as is, even it was
itself a SUSP.

> I have considered
> adding comments to the parts where mutations happen. ///MUTATION!
> append() and join() would then needs such comments, but I think bind() might
> not since the mutations done by join() are entirely /internal/ in
> bind().

Don't do that on my account. I was not complaining about the otherwise
hidden assignment. After all, if everything is truly functional, the
assignment is simply an optimisation.

>> > object parse( object p, object input ){
>> > return p->t == PARSER ? p->Parser.f( p->Parser.v, input ) : NULL;
>> > }
>>
>> I am out of time but for one last remark, and I can make it here though
>> it applies to other functions too. You might find it more helpful, down
>> the line, to replace the
>>
>> return check_type ? something : NULL;
>>
>> pattern with
>>
>> retutn check_type_or_fail(), something;
>
> Do you mean '&&' instead of ','?

No, that would return 0 or 1.

I was not clear enough, by a long chalk. The "_or_fail" in the name was
supposed to suggest a hard fail and an exit(!0) with an error message.
I should also have written it with some arguments. It would probably be
more like

return check_type_or_fail(a, PARSER, __func__), something;

I was in too much of a hurry.

<cut>
--
Ben.

Manfred

unread,
Apr 2, 2019, 8:11:32 AM4/2/19
to
On 4/2/2019 10:25 AM, David Brown wrote:
> If the union part is to be considered a
> hidden internal structure, and everything in user code should use the
> pointer type, then maybe you want:
>
> typedef union object_internal * object;

I see this as probably the better alternative, under the hypothesis.
It could also be
typedef union object_t *object;

The _t suffix is compact enough. It is also used in some standard types,
though, and I am not sure if this is an adverse point.

David Brown

unread,
Apr 2, 2019, 9:51:35 AM4/2/19
to
I like the _t suffix, but people who do POSIX programming often choose
not to use it, because it is reserved for POSIX types (AFAIK - I don't
do POSIX programming in C).

luser droog

unread,
Apr 2, 2019, 3:08:02 PM4/2/19
to
On Tuesday, April 2, 2019 at 2:31:27 AM UTC-5, luser droog wrote:
> On Monday, April 1, 2019 at 5:55:40 PM UTC-5, Ben Bacarisse wrote:
> > This is interesting, and I admire your bravery in wading into these
> > shark-infested waters dressed only in C, so I hope you won't find these
> > remarks overly negative.
>
<snip>
> > Secondly, a monad is very general. I see bind and return functions but
> > they seem to be tied to parsers. Am I to read bind and return as a
> > specific instance of a monad?
>
> Yes. 'result' and 'bind' provide monadic behavior (just) for parsers. I think
> there's also the makings of a list monad in there, too.
>
> So, yes, there's not actually a general Monad that can be instantiated
> with different types. And there's no closures. And of course, there never
> was a lambda. I think the laziness is pretty real, though. I got ideas
> from the classic papers.
>

I think Ben is correct, and I have not actually made a Monad, but just
a Monoid of parsers.

Tim Rentsch

unread,
Apr 3, 2019, 10:59:49 AM4/3/19
to
[trimming without any further indication]

Ben Bacarisse <ben.u...@bsb.me.uk> writes:
> M Joshua Ryan <luser...@gmail.com> writes:
>
>> typedef union object *object;
>
> I'm not a fan of hiding a pointer like this. It tripped me up for
> a while. For the price of a space and a * you lose the ability to
> remind the reader that its pointers all the way down.

I have a different view. When the primary type of an interface
is a pointer type and not the pointed-to type, it's better for
the interface to define a typedef name that includes the *, and
use that name in declaring the various interface functions, both
for parameters and return values. Client code normally uses the
pointer type, not the pointed-to type; following the principle
of information hiding, the pointerness is better held inside the
typedef name than forcing client code to be reminded at every
turn of more information than is needed.

There is a question of which type should be taken as "the primary
type", especially in settings like this one where there is no
explicit line between client code and code for the abstraction
(and certainly no clearcut expression of what constitutes "the
interface" to the abstraction). But look at the source. There
are, not counting the few bottom-level constructor functions,
roughly 30 functions. About two-thirds of those don't access the
pointed-to internals at all, but just pass around values that
come from lower-level functions (or parameters, etc). Most of
the code (counting by number of functions) doesn't need to know,
or care, that "under the hood" the values are actually pointers.
Furthermore any additional code is more likely not to need any
internal access either. So it seems fair to say the primary type
here is the pointer type rather than the pointed-to type.

Incidentally, there is just one function ('new_') that takes the
pointed-to type as a parameter. The source is easily changed so
new_() takes the pointer type instead (please excuse any changes
in whitespace style):

object
new_( object o ){
object p = calloc( 1, sizeof *p );
return p ? *p = *o, p : 0;
}
#define OBJECT(...) new_( (union object[]){{ __VA_ARGS__ }} )

For reference, here is the original (modulo whitespace changes):

object
new_( union object o ){
object p = calloc( 1, sizeof o );
return p ? *p = o, p : 0;
}
#define OBJECT(...) new_( (union object){ __VA_ARGS__ } )

Callers of new_() go through the OBJECT() macro exclusively,
which allows them to work without any further changes (kudos for
good factoring; unfortunately a demerit for using 'sizeof o'
rather than 'sizeof *p').

Ben Bacarisse

unread,
Apr 3, 2019, 11:29:54 AM4/3/19
to
Tim Rentsch <tr.1...@z991.linuxsc.com> writes:

> [trimming without any further indication]
>
> Ben Bacarisse <ben.u...@bsb.me.uk> writes:
>> M Joshua Ryan <luser...@gmail.com> writes:
>>
>>> typedef union object *object;
>>
>> I'm not a fan of hiding a pointer like this. It tripped me up for
>> a while. For the price of a space and a * you lose the ability to
>> remind the reader that its pointers all the way down.
>
> I have a different view. When the primary type of an interface
> is a pointer type and not the pointed-to type, it's better for
> the interface to define a typedef name that includes the *, and
> use that name in declaring the various interface functions, both
> for parameters and return values. Client code normally uses the
> pointer type, not the pointed-to type; following the principle
> of information hiding, the pointerness is better held inside the
> typedef name than forcing client code to be reminded at every
> turn of more information than is needed.

I must say I've flip-flopped on this point over the years so I should
describe my statement as where am I now rather than it being some fixed
view.

I agree with you in one case that I forgot to mention. Where there is a
very clean separation between client code and implementation, and the
client API uses a pointer to an incomplete type (the old "opaque" data),
I will usually make the interface type include the pointer. On the
other side, I probably wouldn't even use the typedef.

<cut>
--
Ben.

Tim Rentsch

unread,
Apr 7, 2019, 3:11:43 AM4/7/19
to
> I will usually make the interface type include the pointer. [...]

Yes, that's pretty much where I am, except that I'm willing to
tolerate pointed-to types are "logically incomplete" and only
"almost always" treated as opaque.

luser droog

unread,
Apr 13, 2019, 2:20:57 AM4/13/19
to
I appreciate the keen insights from all parties here. This is the very sticking
point I seem to have come to. How to make a clean interface over all this stuff.
I keep peeking through the abstractions for quick shortcuts and even though it
all kinda works, there's an appearance of delicacy and fragility I fear. Type
safety is chucked in favor of conventions, but then those conventions might be
chucked in places in favor of hackitude or sthg.

I ran into similar trouble in the past with the multidim arrays.

https://codereview.stackexchange.com/a/122100/5912

Chad

unread,
Apr 13, 2019, 7:42:57 AM4/13/19
to
Not that I'm an expert on this kind of stuff, but I've seen people read some academic books or papers on this kind of stuff before attempting to implement something like this.
0 new messages