Generic member of recursive struct

308 views
Skip to first unread message

Michael Ellis

unread,
Dec 20, 2021, 9:20:26 AM12/20/21
to golang-nuts
I've got a package, github.com/Michael-F-Ellis/goht, that supports creating HTML docs in Go.  It works well, for my purposes at least, but I've always been bothered by having to use []interface{} to define the struct member, C, that supports recursion.

type HtmlTree struct {
        T     string        // html tagname, e.g. 'head'
        A     string        // zero or more html attributes, e.g 'id=1 class="foo"'
        C     []interface{} // a slice of content whose elements may be strings or *HtmlTree
        empty bool          // set to true for empty tags like <br>
}

I've created a minimal extract of the package on the playground at https://go.dev/play/p/-_7JKRZYLC_J?v=gotip.  Assuming the new generics can support constraining HtmlTree.C to be a slice of string | *HtmlTree, what is the right syntax for doing so?

Thanks!


Axel Wagner

unread,
Dec 20, 2021, 9:47:47 AM12/20/21
to Michael Ellis, golang-nuts
They can't, sorry.
 

Thanks!


--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/a8817b41-85dd-4fd1-a946-00764713c008n%40googlegroups.com.

Michael Ellis

unread,
Dec 20, 2021, 10:10:44 AM12/20/21
to golang-nuts
> They can't, sorry.
Ok. Thanks, Axel. 
Saves me wasting more time.  In the past 3 years of using Go, this is the only use case where I've  really wanted generics (other cases I've encountered so far are easily handled with code generation). 

Axel Wagner

unread,
Dec 20, 2021, 10:25:26 AM12/20/21
to Michael Ellis, golang-nuts
Just to be clear, the way I understood you is that you want HtmlTree.C to be a slice which has elements which can each either be a string or an *HtmlTree - i.e. you wan these to be mixed in a single slice. Correct?
Because that is not a use case for generics, it's a use case for sum types (which Go does not have).

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.

Michael Ellis

unread,
Dec 20, 2021, 1:07:21 PM12/20/21
to golang-nuts
>Just to be clear, the way I understood you is that you want HtmlTree.C to be a slice which has elements which can each either be a string or an *HtmlTree - i.e. you want these to be mixed in a single slice. Correct?

Actually, no.  An HtmlTree instance whose content is a string is a terminal node.  The recursion in the Render() func looks like:

      for _, c := range h.C {
                switch c := c.(type) {
                case string:
                        b.WriteString(c)
                case *HtmlTree:
                        err = Render(c, b, rindent)
                        if err != nil {
                                return fmt.Errorf("%s : %v", h.T, err)
                        }
                default:
                        return fmt.Errorf("Bad content %v. Can't render type %T! ", h.C, c)
                }
        }


I suppose it's still a problem, though, since the compiler doesn't have any way of knowing that's how trees of HtmlTree meant to be constructed. I should have expressed the intended constraint on HtmlTree.C as `string | []*HtmlTree`.  Does that make a difference?

Robert Engels

unread,
Dec 20, 2021, 1:33:49 PM12/20/21
to Michael Ellis, golang-nuts
You should use interfaces and a “node” type. 

On Dec 20, 2021, at 12:07 PM, Michael Ellis <michael...@gmail.com> wrote:

>Just to be clear, the way I understood you is that you want HtmlTree.C to be a slice which has elements which can each either be a string or an *HtmlTree - i.e. you want these to be mixed in a single slice. Correct?

Michael Ellis

unread,
Dec 20, 2021, 1:51:42 PM12/20/21
to golang-nuts
On Monday, December 20, 2021 at 1:33:49 PM UTC-5 ren...@ix.netcom.com wrote:
You should use interfaces and a “node” type.

Hmm, I tried 

type Node interface {
        string | []*HtmlTree

}

type HtmlTree struct {
        T     string // html tagname, e.g. 'head'
        A     string // zero or more html attributes, e.g 'id=1 class="foo"'
        C     Node   // a slice of content whose elements may be strings or *HtmlTree

        empty bool   // set to true for empty tags like <br>
}

and the compiler said: 
./prog.go:21:8: interface contains type constraints.

(line 21 is the declaration of HtmlTree.C as type Node)

What's the syntax you have in mind?


 

Robert Engels

unread,
Dec 20, 2021, 2:17:39 PM12/20/21
to Michael Ellis, golang-nuts
You create structs like StringNode and HtmlNode that implement the common needed operations. 

Imagine a Render() method for a string you convert to html, for an html node you render recursively. 

Or similar. If an HtmlNode is not a leaf node it needs recursive operations. 

On Dec 20, 2021, at 12:52 PM, Michael Ellis <michael...@gmail.com> wrote:


--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.

Axel Wagner

unread,
Dec 20, 2021, 2:29:48 PM12/20/21
to Michael Ellis, golang-nuts
On Mon, Dec 20, 2021 at 7:07 PM Michael Ellis <michael...@gmail.com> wrote:
>Just to be clear, the way I understood you is that you want HtmlTree.C to be a slice which has elements which can each either be a string or an *HtmlTree - i.e. you want these to be mixed in a single slice. Correct?

Actually, no.  An HtmlTree instance whose content is a string is a terminal node.  The recursion in the Render() func looks like:

      for _, c := range h.C {
                switch c := c.(type) {
                case string:
                        b.WriteString(c)
                case *HtmlTree:
                        err = Render(c, b, rindent)
                        if err != nil {
                                return fmt.Errorf("%s : %v", h.T, err)
                        }
                default:
                        return fmt.Errorf("Bad content %v. Can't render type %T! ", h.C, c)
                }
        }

That's pretty much what I meant, yes. You want a sum type (and in lieu of sum types, an interface), not generics. 
The "union" aspect of Go generics (the `a | b` syntax) only applies to using interfaces as *constraints*, not as *types* - you want to do the latter. You want to have a field which is either a string or an *HtmlNode - but Go only gives you a convenient way to write the two distinct types `type HtmlNodeString struct { /*…*/ C string }` and `type HtmlNodeNode struct { /* … */ C *HtmlNode }` into one¹ type declaration and write function which can work on either. It's just not what you want.

The best option for you is (as Robert mentions) to use an interface, which is an "open sum" - meaning you can't have the guarantee that its dynamic type is of a limited set of options, but you *can* have a value whose actual type is dynamic. You can look at e.g. ast.Node, which is de-facto the same thing - it's supposed to be a limited set of options, but in lieu of sum types, it's an interface.

Go might, at some point, allow the union-elements of constraint interfaces to be used as actual types, which *would* be a form of sum types. But probably not for a while - it's more complicated than it may seem.



I suppose it's still a problem, though, since the compiler doesn't have any way of knowing that's how trees of HtmlTree meant to be constructed. I should have expressed the intended constraint on HtmlTree.C as `string | []*HtmlTree`.  Does that make a difference?
On Monday, December 20, 2021 at 10:25:26 AM UTC-5 axel.wa...@googlemail.com wrote:
Just to be clear, the way I understood you is that you want HtmlTree.C to be a slice which has elements which can each either be a string or an *HtmlTree - i.e. you wan these to be mixed in a single slice. Correct?
Because that is not a use case for generics, it's a use case for sum types (which Go does not have).

On Mon, Dec 20, 2021 at 4:11 PM Michael Ellis <michael...@gmail.com> wrote:
> They can't, sorry.
Ok. Thanks, Axel. 
Saves me wasting more time.  In the past 3 years of using Go, this is the only use case where I've  really wanted generics (other cases I've encountered so far are easily handled with code generation). 

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.

Axel Wagner

unread,
Dec 20, 2021, 2:31:32 PM12/20/21
to Michael Ellis, golang-nuts
Oh, forgot the footnote:

[1] Note that even that doesn't *actually* work, as `*HtmlNode` would become a parametric type, so it would have to be instantiated to be used in the constraint - you'd have to write `type HtmlNode[T string | *HtmlNode[Something]]`. And the fact that there is no actual answer to what "Something" would be should be a strong indicator for how much generics are not what you want for this.
Message has been deleted

Robert Engels

unread,
Dec 20, 2021, 4:17:19 PM12/20/21
to Michael Ellis, golang-nuts
This is a task for a parser - and the parser can be type safe -but the constraints for something as complex as html aren’t typically  expressed in types but rather bnf or similar. 

You can look at the Java html renderer/parser and it uses type safe nodes but it still has a resilient parser layer to produce the nodes. 



On Dec 20, 2021, at 3:06 PM, Michael Ellis <michael...@gmail.com> wrote:

Thanks, Axel & Robert.  As I said in the first post, the package already works well for my purposes (and apparently for the few others who use it).  It allows defining web content more concisely than raw HTML and makes the full power of Go available for composing repetitive page structures.

I was mostly just curious about the possibility of detecting bad content at compile time instead of at run time. In practice, that's turned out not to be a significant problem so I'm ok with learning generics don't support a fix.
Reply all
Reply to author
Forward
0 new messages