All your connection are belong to us

151 views
Skip to first unread message

Mark Friedman

unread,
Oct 27, 2021, 3:19:22 PM10/27/21
to blo...@googlegroups.com
In a separate thread in this group there has been some discussion about how one might create a "chameleon" block that could change shape between a statement (i.e. has next and previous connections) and a value (i.e. has an output connection). As part of that process I did some playing around with trying to create a block that was both a statement and a value.  It turns out that it's somewhat difficult to do because you can't just use setOutput(true) because it will throw an error if the block has a previous connection.  Similarly, you can't add a previous connection if you already have an output connection.  However, you can add an output connection using makeConnection_() after creating a previous connection!

However, even though Blockly allows you to create a block that has all the connections, there is other code that assumes that you do not have both output and previous connections; for example, Block.unplug().

So, my first question is: where's the bug?  Is it that we shouldn't, in fact, be allowed to add an output connection if a previous connection already exists or is it that there should not be code that assumes that only have either a previous connection or an output connection?  If it is the former, then I would ask why? And if previous+output connections are intended to be disallowed, why not next+output connections?  Or is that another bug?

Thanks in advance.

-Mark

Beka Westberg

unread,
Oct 28, 2021, 10:43:29 AM10/28/21
to blo...@googlegroups.com
I think the problem is with using makeConnection_ :/ If you check that docs that's a protected function which isn't intended to be called by external developers. All of the public functions try to protect you from having both an output and a previous connection, which is an invariant that Blockly expects.

I believe the reason that this is an invariant is because handling both types of connections on a single block adds the possibility of circular connections, which adds a lot of additional complexity to Blockly. But again, I don't remember the exact context here.

> And if previous+output connections are intended to be disallowed, why not next+output connections?

The way I think of it is that these two pairs of connection types are associated:
output : previous
input : next

Previous connections are often used to model "outputs" in situations where you can have a variable number of inputs. For example, open roberta uses statement inputs to model variable declarations, which are very similar to outputs:
open-roberta.png
As such we treat previous connections as passing information "up" to the parent block, the same way that outputs pass information "into" the parent block.

Next inputs act the opposite way, accepting information, in the same way that input connections do.

Granted this is pretty confusing because the visualization of the connections makes it seem to be the opposite, but this is how it works internally.

I hope that kinda helps? But it's confusing to be honest haha. Happy to talk about it more if you have other thoughts!
--Beka

--
You received this message because you are subscribed to the Google Groups "Blockly" group.
To unsubscribe from this group and stop receiving emails from it, send an email to blockly+u...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/blockly/CAOk7GcTYLq2ERy52mHxMMJ0RHR3y5d4f%2BLzjjMCJy6T4XB1%2BZA%40mail.gmail.com.

Coda Highland

unread,
Oct 28, 2021, 11:00:40 AM10/28/21
to blo...@googlegroups.com
I think that's not the right place to be pointing fingers, because while makeConnection_ is an internal API, Blockly.Connection's constructor is not. I also can't imagine that the concern is circular connections, because you can get circular connections with previous+next, the combination of which is obviously a supported use case and for which Blockly doesn't even try to validate for circular connections.

Based on my understanding of the implementation, I would guess the bigger concern is the notion of a block's "parent." A block can obviously only have one parent, and that parent is the one linked by the previous or output connection. If both connections were concurrently in use, the parent-child relationship would become inconsistent for one of the two connected blocks -- that is, it appears that the invariant being maintained is `block.getChildren().every(child => child.getParent() === block)`.

This, on the other hand, is a significantly easier invariant to maintain. You don't have to prohibit the existence of previous+output. You only have to prohibit having both of them connected at once, which is the desired behavior anyway. ConnectionChecker can surely be made to do this efficiently.

/s/ Adam

Mark Friedman

unread,
Oct 28, 2021, 1:34:00 PM10/28/21
to blo...@googlegroups.com
On Thu, Oct 28, 2021 at 7:43 AM Beka Westberg <bekawe...@gmail.com> wrote:
I think the problem is with using makeConnection_ :/ If you check that docs that's a protected function which isn't intended to be called by external developers. All of the public functions try to protect you from having both an output and a previous connection, which is an invariant that Blockly expects.

I believe the reason that this is an invariant is because handling both types of connections on a single block adds the possibility of circular connections, which adds a lot of additional complexity to Blockly. But again, I don't remember the exact context here.

Adam had an excellent reply about this having more to do with parentage and how that would be better enforced when blocks actually get connected.
 

> And if previous+output connections are intended to be disallowed, why not next+output connections?

The way I think of it is that these two pairs of connection types are associated:
output : previous
input : next

Previous connections are often used to model "outputs" in situations where you can have a variable number of inputs. For example, open roberta uses statement inputs to model variable declarations, which are very similar to outputs:
open-roberta.png
As such we treat previous connections as passing information "up" to the parent block, the same way that outputs pass information "into" the parent block.

Next inputs act the opposite way, accepting information, in the same way that input connections do.

Granted this is pretty confusing because the visualization of the connections makes it seem to be the opposite, but this is how it works internally.

As you remark, this is confusing and unintuitive.  Plus in this case at least, it's not showing a use for previous+output connections. 


I hope that kinda helps? But it's confusing to be honest haha. Happy to talk about it more if you have other thoughts!

Would the team be open to reconsidering these decisions (assuming they are not just accidental implementation details)?

-Mark

ewpa...@gmail.com

unread,
Oct 28, 2021, 3:50:34 PM10/28/21
to Blockly
I thought this was possible once upon a time. I recall Erik saying that it was a headache because while most people assumed blocks would be either statements or values, the fact that people could create a block that was essentially both was a challenge. I can see the potential benefit for it though were the semantics of some languages allow for statements that can also serve as values. In App Inventor of course we have to hack this by providing two variants of blocks, such as the blocks that introduce lexical scoping (the last statement in a Scheme let is also its value), but since sometimes one doesn't care about the value produced the statement-like block form is more appropriate.

Cheers,
Evan

Mark Friedman

unread,
Oct 28, 2021, 5:02:26 PM10/28/21
to blo...@googlegroups.com
Sure, I get that for the classic use case of Blockly with non-programmers you might want to avoid the confusion of a block being both a statement and a value but I believe that Blockly should be a library that allows other, more advanced, use cases and to allow for visual programming language developers to make that choice for themselves.  

-Mark


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

feni...@google.com

unread,
Oct 29, 2021, 3:35:01 PM10/29/21
to Blockly
Hey all! 

I just caught up on this thread (and the thread on chameleon blocks). Some responses to factual questions:

- You can create a block with an output and a next connection. Many years ago I tried to disallow this, and learned that a few developers were using it to represent a promise, so we kept it.
- You cannot create a block with an output and a previous connection. But as you all have noted, those prohibitions fade depending on which level you are at in the code. Blockly's leaky API strikes again.
- The issue here is absolutely parentage, and keeping the invariant that a block can only have one parent. 

> This, on the other hand, is a significantly easier invariant to maintain. You don't have to prohibit the existence of previous+output. You only have to prohibit having both of them connected at once, which is the desired behavior anyway. ConnectionChecker can surely be made to do this efficiently.
- The connection checker API should allow you to make those checks, yes.

Now some musings:
- Representing a block with an ignorable return value feels tricky on a user interface level. How do you give feedback to a user that they must disconnect their previous connection before connecting the output? Does your block have both all the time, or change its rendering as you drag? The ternary block is an example where it may change types--and disconnect blocks--in response to blocks being dragged near it, and it feels like everything has exploded if you don't know why that's happening. Same thing with figuring out how to heal stacks, show insertion markers, etc. 

- Is it feasible to work around this?
Probably not currently, but exploration can tell us more about which code paths are blocking. The assumptions here are baked very deep into Blockly, and all over the place. We would need to do a lot of internal changes to have a single place where this invariant is applied (and could thus be removed) before it would be externally feasible. The first step would be to write tests to verify the existing behaviour, and note carefully which parts of it you actually want to change. For instance, everything around unplugging and healing connections would need tests to make sure we aren't accidentally changing behaviour.

- Would the team be open to reconsidering those decisions? 
I'm not completely opposed, but I am very wary because of the combination of difficult UI and deep changes.

The use case you gave in the chameleon blocks thread is definitely compelling.

Rachel

Coda Highland

unread,
Oct 29, 2021, 4:03:48 PM10/29/21
to blo...@googlegroups.com
On Fri, Oct 29, 2021 at 2:35 PM 'feni...@google.com' via Blockly <blo...@googlegroups.com> wrote:
Now some musings:
- Representing a block with an ignorable return value feels tricky on a user interface level. How do you give feedback to a user that they must disconnect their previous connection before connecting the output? Does your block have both all the time, or change its rendering as you drag? The ternary block is an example where it may change types--and disconnect blocks--in response to blocks being dragged near it, and it feels like everything has exploded if you don't know why that's happening. Same thing with figuring out how to heal stacks, show insertion markers, etc. 

I would strongly recommend having it change its rendering to indicate which connection it has / is about to have. That fully solves the "what if I do both?" issue.

I think the ternary block (I assume you mean IIF() / ?: by this) isn't really something we need to worry about. It's one thing to have a block that has both top and left connections. It's another to have an INPUT that can ACCEPT both value and statement blocks in the same connection. I see no reason to support the latter; that WOULD be more confusing. And since a conditional block that returns a value would accept values, and a conditional block that acts as a statement would accept statements, it stands to reason that you would have two separate blocks for the task even if "chameleon" blocks were to exist.
 
/s/ Adam

Mark Friedman

unread,
Oct 29, 2021, 6:36:04 PM10/29/21
to blo...@googlegroups.com
+1

-Mark


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

Mark Friedman

unread,
Oct 29, 2021, 6:41:02 PM10/29/21
to blo...@googlegroups.com
Rachel,

  Thanks for your thoughtful response to all this.  I'll probably do some more experimenting and see what I can come up with.  If I think I have enough for a PR, I'll submit one, and you can decide at that point whether it is worthy.  Otherwise, I suppose I can live with monkey patching (assuming I can come up with something that works), but I'd rather not ;-). If you, or anyone else, can point out potential gotchas I would greatly appreciate it.

-Mark


Neil Fraser

unread,
Nov 1, 2021, 2:40:51 AM11/1/21
to Blockly
It may be relevant that the sample blocks contains this shape-changer:
Screen Shot 2021-10-31 at 23.36.16.png
These two are the same block, it modifies its parent connections depending on the selected option in the first dropdown.

So mutating a block from statement to value block is something that's always been possible.

feni...@google.com

unread,
Nov 1, 2021, 12:56:03 PM11/1/21
to Blockly
> I think the ternary block (I assume you mean IIF() / ?: by this) isn't really something we need to worry about. It's one thing to have a block that has both top and left connections. It's another to have an INPUT that can ACCEPT both value and statement blocks in the same connection. I see no reason to support the latter; that WOULD be more confusing. And since a conditional block that returns a value would accept values, and a conditional block that acts as a statement would accept statements, it stands to reason that you would have two separate blocks for the task even if "chameleon" blocks were to exist.

The ternary block is relevant here because its connections change in response to what is being dragged to it. The changes are to connection type instead of connection existence, but it's still a good case to look at since those changes sometimes cause it to spit out already connected blocks. Since insertion markers are actual blocks that are connected during a drag and then disconnected as the dragging block moves away, there's some tricky undo/redo behaviour for those rendering changes (referred to as "healing the stack" in code).

> I would strongly recommend having it change its rendering to indicate which connection it has / is about to have. That fully solves the "what if I do both?" issue.

This may fully solve one problem while creating a myriad of other problems. But again, I think you/Mark should experiment with it.

Cheers,
Rachel


Coda Highland

unread,
Nov 1, 2021, 1:13:11 PM11/1/21
to blo...@googlegroups.com
On Mon, Nov 1, 2021 at 11:56 AM 'feni...@google.com' via Blockly <blo...@googlegroups.com> wrote:
> I think the ternary block (I assume you mean IIF() / ?: by this) isn't really something we need to worry about. It's one thing to have a block that has both top and left connections. It's another to have an INPUT that can ACCEPT both value and statement blocks in the same connection. I see no reason to support the latter; that WOULD be more confusing. And since a conditional block that returns a value would accept values, and a conditional block that acts as a statement would accept statements, it stands to reason that you would have two separate blocks for the task even if "chameleon" blocks were to exist.

The ternary block is relevant here because its connections change in response to what is being dragged to it. The changes are to connection type instead of connection existence, but it's still a good case to look at since those changes sometimes cause it to spit out already connected blocks. Since insertion markers are actual blocks that are connected during a drag and then disconnected as the dragging block moves away, there's some tricky undo/redo behaviour for those rendering changes (referred to as "healing the stack" in code).

Well, that's what I'm saying -- if we're worried about things being confusing, I think that's the wrong design for a conditional block. Sure, having the same block definition work for both cases is fine, but I think that having statement-if and expression-if being a single block in the toolbox that dynamically mutates back and forth based on its children is probably not the best idea. It doesn't make sense to me to have an input that can be either a value input or a statement input. They're not even rendered the same way! Values go inside the body of the block; statements go in the cavity of a C shape.
 
/s/ Adam

Mark Friedman

unread,
Nov 2, 2021, 6:14:13 PM11/2/21
to blo...@googlegroups.com
Thanks, Neal.  I'll take a look.

-Mark


Mark Friedman

unread,
Nov 2, 2021, 6:48:02 PM11/2/21
to blo...@googlegroups.com
On Mon, Nov 1, 2021 at 10:13 AM Coda Highland <chig...@gmail.com> wrote:
On Mon, Nov 1, 2021 at 11:56 AM 'feni...@google.com' via Blockly <blo...@googlegroups.com> wrote:
> I think the ternary block (I assume you mean IIF() / ?: by this) isn't really something we need to worry about. It's one thing to have a block that has both top and left connections. It's another to have an INPUT that can ACCEPT both value and statement blocks in the same connection. I see no reason to support the latter; that WOULD be more confusing. And since a conditional block that returns a value would accept values, and a conditional block that acts as a statement would accept statements, it stands to reason that you would have two separate blocks for the task even if "chameleon" blocks were to exist.

The ternary block is relevant here because its connections change in response to what is being dragged to it. The changes are to connection type instead of connection existence, but it's still a good case to look at since those changes sometimes cause it to spit out already connected blocks. Since insertion markers are actual blocks that are connected during a drag and then disconnected as the dragging block moves away, there's some tricky undo/redo behaviour for those rendering changes (referred to as "healing the stack" in code).

Well, that's what I'm saying -- if we're worried about things being confusing, I think that's the wrong design for a conditional block. Sure, having the same block definition work for both cases is fine, but I think that having statement-if and expression-if being a single block in the toolbox that dynamically mutates back and forth based on its children is probably not the best idea.

Just to be clear, that's not what the current ternary block does nor is it what I am proposing.  I am proposing a block which indicates that it can act as either a statement or an expression depending on where it is placed, not what it's children are.  That said, I don't really see anything wrong conceptually with having the latter, though I can't quite come up with a reasonable use-case off the top of my head ;-)
 
It doesn't make sense to me to have an input that can be either a value input or a statement input.
 
I'm not sure that anyone has proposed such a "chameleon input".

They're not even rendered the same way!
 
I could imagine a renderer which did not have that quality.  For example, I could imagine a renderer where all blocks and inputs were simple rectangles.

Values go inside the body of the block; statements go in the cavity of a C shape.

I'll just point out that values could be considered to be "outside" the body of the block if the inputs are external and that you can have statement stacks that are not contained within C shapes. Nevertheless your point about statement inputs and value inputs being rendered differently (with the current renderers) still holds.

Take care.

-Mark

Mark Friedman

unread,
Nov 2, 2021, 6:50:38 PM11/2/21
to blo...@googlegroups.com
Mea culpa for the name typo, Neil :-(

-Mark

Coda Highland

unread,
Nov 4, 2021, 10:23:24 AM11/4/21
to blo...@googlegroups.com
On Tue, Nov 2, 2021 at 5:48 PM Mark Friedman <mark.f...@gmail.com> wrote:
On Mon, Nov 1, 2021 at 10:13 AM Coda Highland <chig...@gmail.com> wrote:
On Mon, Nov 1, 2021 at 11:56 AM 'feni...@google.com' via Blockly <blo...@googlegroups.com> wrote:
> I think the ternary block (I assume you mean IIF() / ?: by this) isn't really something we need to worry about. It's one thing to have a block that has both top and left connections. It's another to have an INPUT that can ACCEPT both value and statement blocks in the same connection. I see no reason to support the latter; that WOULD be more confusing. And since a conditional block that returns a value would accept values, and a conditional block that acts as a statement would accept statements, it stands to reason that you would have two separate blocks for the task even if "chameleon" blocks were to exist.

The ternary block is relevant here because its connections change in response to what is being dragged to it. The changes are to connection type instead of connection existence, but it's still a good case to look at since those changes sometimes cause it to spit out already connected blocks. Since insertion markers are actual blocks that are connected during a drag and then disconnected as the dragging block moves away, there's some tricky undo/redo behaviour for those rendering changes (referred to as "healing the stack" in code).

Well, that's what I'm saying -- if we're worried about things being confusing, I think that's the wrong design for a conditional block. Sure, having the same block definition work for both cases is fine, but I think that having statement-if and expression-if being a single block in the toolbox that dynamically mutates back and forth based on its children is probably not the best idea.

Just to be clear, that's not what the current ternary block does nor is it what I am proposing.  I am proposing a block which indicates that it can act as either a statement or an expression depending on where it is placed, not what it's children are.  That said, I don't really see anything wrong conceptually with having the latter, though I can't quite come up with a reasonable use-case off the top of my head ;-)

I know that's what you meant, but that was in response to Rachel.

I'm not sure which project has the ternary block she's talking about, though.

/s/ Adam

feni...@google.com

unread,
Nov 4, 2021, 11:45:57 AM11/4/21
to Blockly
I'm talking about the block in the attached image. 
7onD39ZwyjKLgjY.png

Coda Highland

unread,
Nov 4, 2021, 6:46:30 PM11/4/21
to blo...@googlegroups.com
That's the block I thought you meant, but I don't understand how that block changes connections based on what you attach to it. It always seems to accept three value inputs and has an output connection.

/s/ Adam

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

Rachel Fenichel

unread,
Nov 4, 2021, 6:52:00 PM11/4/21
to blo...@googlegroups.com
The connection types don't change (value input vs statement input, etc) but the checks do change in a way that causes it to reject blocks without changing shape.

The " " blocks have output connections with a "string" type check. If you attach both to the ternary block, the output of the ternary block gets a "string" check as well.

If you then drag the ternary block into a place that only accepts booleans (and block), the ternary block will happily connect--but then it disconnects the " " blocks, because their checks no longer match.

You received this message because you are subscribed to a topic in the Google Groups "Blockly" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/blockly/mmZcSqoGun8/unsubscribe.
To unsubscribe from this group and all its topics, send an email to blockly+u...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/blockly/CAFE_sBEy7KXmtre1FoG%3Dc0v060DF%2BbnFDLxbO1Jn%2Bn_1Doe02Q%40mail.gmail.com.
7QSwpZRezdE4HoP.png

Coda Highland

unread,
Nov 4, 2021, 8:23:06 PM11/4/21
to blo...@googlegroups.com
Ah! Okay, I see what you're saying, then. So from the perspective of giving inspiration on the kinds of circumstances you need to watch out for, the advice is well-given. So... Thinking through it: 

As long as the exposed connections are a function of the things it's connecting to instead of a function of the things connected to it, it should be pretty robust -- it's not going to change connections when it's already hooked up in a program.

It's possible that the connection compatibility might change based on attached blocks, but I don't think that's going to introduce any wrinkles that aren't already addressed. If it gets ejected it'll go back to being able to connect as either a value or a statement, but it won't automatically reconnect until the user picks it up and drags it somewhere.

/s/ Adam

Mark Friedman

unread,
Nov 15, 2021, 4:05:59 PM11/15/21
to blo...@googlegroups.com
For those who are interested, I have a pull request pending for this here.

-Mark

Coda Highland

unread,
Nov 15, 2021, 8:03:28 PM11/15/21
to blo...@googlegroups.com
I think I disagree with changing the behavior of setOutput() and setPreviousStatement() without an API change. This would be a breaking change for blocks that default to one configuration and switch to the other using a mutator. That code would expect that setOutput() would switch it from a statement block to a value block, and that setPreviousStatement() would switch it from a value block to a statement block.

I can think of a few possible APIs that would avoid breaking compatibility.

(1) Expose a symbolic constant that could be passed as the first parameter that acts like `true` but suppresses the disconnect behavior. This would be a relatively non-invasive change and the resulting code would be pretty readable. However, I also really don't like overloading boolean flags like that. It carries a hint of code smell, and while I can't put my finger on exactly why it's bad, my intuition is yelling at me. (Perhaps it could change to accepting an enum, but the first two values of the enum are false and true for backwards compatibility?)

(2) Add a setOutputAndPreviousStatement() method. Aside from it being a clumsy method name, I don't like the behavior of the connection check parameter for this idea, but those are the only drawbacks I see.

(3) Add a setConnections() method that would accept a configuration object. The existing setOutput, setPreviousStatement, and setNextStatement methods would then become wrappers around this method. I kind of like this from a maintenance perspective, as it reduces the number of functions that mutate the connection configuration directly.

(4) Add a boolean flag (that I don't have a good name for off the top of my head) that would enable or disable the behavior of removing the other connection. The default would preserve the existing behavior. I could see arguments for this being a workspace-level setting or a block-level setting, but I lean towards it being block-level. That said, I don't especially like flags that change behavior like this because of the separation between cause and effect.

I also wonder if it might be a good idea to split the PR up into two parts -- one part that fixes up the connection checker, which can be merged independently without breaking compatibility and would permit chameleon blocks to be implemented in downstream code, and one that adds the chameleon blocks themselves.

/s/ Adam


Mark Friedman

unread,
Nov 16, 2021, 2:32:02 PM11/16/21
to blo...@googlegroups.com
I'm a little confused.  The current behavior of setOutput() and setPreviousStatement() is not what you describe.  setOutput() does not switch from a statement block to a value block; it just adds or removes the output plug. And setPreviousStatement() does not switch from a value block to a statement block; it just adds or removes the notch.  Am I misunderstanding something?

-Mark


Coda Highland

unread,
Nov 16, 2021, 2:48:10 PM11/16/21
to blo...@googlegroups.com
Uh... The misunderstanding may be mine. My apologies in retrospect; I realize that I confused myself! For some reason I thought that the old implementation removed the conflicting connection instead of throwing.

/s/ Adam

Mark Friedman

unread,
Nov 21, 2021, 3:25:04 PM11/21/21
to blo...@googlegroups.com
Again, for those who are interested, there is another pull request related to my quest for a "chameleon" block here.  This one addresses the desire for the set of connections on the block to be able to change more dynamically (i.e. when dragged out of its context) and have the insertion marker manager deal with it more reasonably.

-Mark

Mark Friedman

unread,
Jan 5, 2022, 6:52:10 PM1/5/22
to blo...@googlegroups.com
Another FYI for those who might be following this.  The pull requests mentioned below have been approved and merged.  The initial one relating to setOutput is already in the current Blockly release. The other one, relating to more dynamic connection checker behavior, isn't yet in the released version of Blockly but should, I assume, be in the next one.

-Mark

Reply all
Reply to author
Forward
0 new messages