Unexpected broadcasting behavior involving eachrow()

Macros do not break referential transparency. The value of the expression given to the macro is the code/symbols themselves (not what they would return when executing). So if you have an outer macro that takes the result of an inner macro as its argument, and the inner macro does return the same string of symbols that you could have literally written in the code as an argument, then the outer macro do give the same result (i.e., does not break referential transparency). So macros are not inherently breaking referential transparency, they just take their arguments at another time, and so in the phrase “An expression is called referentially transparent if it can be replaced with its corresponding value (and vice-versa) without changing the program’s behavior.” an “expression” must be another macro, and the “value of the expression” is a string of symbols that could have written directly into the code, and yes, those two are interchangeable to the outer macro (i.e., the program will run the same way) if they output the same string of symbols.

In Julia, the only convention related to “referential transparency” is that the name of methods that mutate their arguments should end with a !. And this is not even followed by the IO functions of the standard library (it is assumed that all of them break “referential transparency”). Many languages do not have a strong convention about that, and do not make an effort to distinguish between functions that have referential transparency or not. Because in most languages, breaking referential transparency is exceedingly common, the programmer just needs to write a mutating procedure (which again, outside of functional languages, it is a very typical characteristic of a procedure).

Maybe I am not using the term “referential transparency” correctly; I mean to use it in a more human/subjective sense than theoretical. When I say it doesn’t always hold for macros I mean something like how usually if x = (arg1, arg2, arg3), then we can pass in foo(x...) just fine, since x... can be replaced by its corresponding value, an argument list. On the other hand,

@evalpoly(1,2,3) # works
@evalpoly(x...) # breaks

But since there is that flashing neon @, I know to expect non-standard evaluation and don’t mind the lack of “referential transparency,” or whatever term you prefer we use to describe that expectation.

Because in most languages, breaking referential transparency is exceedingly common

I assume you are referring to functions that modify mutable/global state? I do appreciate very much that Julia has a naming convention that warns users when this happens, but it’s not really what I’m referring to. It might just be my own ignorance, and feel free to correct me, but I don’t think I’ve personally ever used a language that breaks the “transparency” at a syntax level as fundamental as = or ().

As I’ve been corrected many times above, I know that .= is not the same as = and in fact should be treated differently; it’s this very distinction I think requires larger hazard signs to be present in the docs, since the symbols just look so similar and behave so similarly in most cases I think it’s a reasonable expectation.

With =, I (correctly) expect the following to all leave var with the same value

var = expr
var = (expr)
var = identity(expr)
var = only(Ref(expr))
var = [expr][1]

let tmp = expr
    var = tmp
end

The part that appears to break transparency to me is that when using .= for var and expr having equal shapes, suddenly only some of those formulations do what I expect, and the others are broken different.

The problem is: if we do not define “referential transparency” formally, then we cannot discuss if other languages break it or not;

Going again with the technical definition of “An expression is called referentially transparent if it can be replaced with its corresponding value (and vice-versa) without changing the program’s behavior.”, then = is not referential transparent either, because the expression x = y cannot be just replaced by y without changing the program (x will not change value anymore what changes the semantics of the program). And this is true for basically any language in which x = y is an expression, not just an statement.

I am not sure any of these examples you gave have anything to do with referential transparency. Maybe we need a better term to describe whatever implicit rule .= is breaking, because for me, it is just the fact you changed one operator for another another completely different, that does not bind a name but takes a mutable object as its first argument and mutate it, and therefore is subject the same aliasing problems any procedure that mutates its arguments have.

1 Like

You’re right, reassignments are not referentially transparent because the variable’s value is changing so it and expressions involving it cannot be substituted by a value. That’s why some functional languages don’t allow reassignment. adienes was told the term “referential transparency” earlier and that was probably not actually what they meant by val = mean(x); x = val being equivalent to x = mean(x).

That just does not imply val = mean.( (x,) ); x .= val is equivalent to x .= mean.( (x,) ). The desired property is in the latter, just for = per iteration: val = mean(x); x[i] = val is equivalent to x[i] = mean(x). Moreover, val = mean.( (x,) ); x = val is equivalent to x = mean.( (x,) ). The dotted .= is not interchangeable with =; a loop of elementwise assignments does not have the same property as 1 variable assignment.

1 Like

I do not know, maybe this is too simplistic but I think it boils down to:

julia> [1, 2, 3] .= [4, 5, 6]
3-element Vector{Int64}:
 4
 5
 6

julia> [1, 2, 3] = [4, 5, 6]
ERROR: syntax: invalid assignment location "[1, 2, 3]" around REPL[3]:1
Stacktrace:
 [1] top-level scope
   @ REPL[3]:1

= is impossible to implement as a method, because it does not take x as any value/object bound to that name (which in fact, may not exist yet, the name may be undefined before, we may be declaring it at that point), instead, it takes x as a symbol like a macro would.

.=, on the other hand, is just a method, and it does not care or even is aware if the first operand (LHS) is a name, a literal, or just any expression that returns a value, it just touches the values of both sides, as any method (and most operators) do.

2 Likes

When broadcasting gets too complicated, I often find it simpler, clearer, and maybe event more performant to just write the for loop. To me white space and structure is clearer than parantheses.

5 Likes