i get the feeling there aren’t strict separation b...
# thinking-together
j
i get the feeling there aren’t strict separation between declarative and imperative. it’s a spectrum and real world languages fall somewhere on the spectrum
d
It seems to me that at the conceptual level there is a clear, grammatical distinction. It’s easy to detect in code with the following test: does execution order of a series of expressions matter? If so, it’s imperative. But it also appears to me that there is a kind of “coinherence” between knowledge and imperative knowledge (to borrow the term from theology) - knowledge of what and how seem inextricably embedded in each other, though definitely distinct. I don’t know if this coinherence has any manifestation in formal logic, but wouldn’t be at all surprised to find it.
b
@Daniel Hines Isn't that the distinction we already have with pure functions / side effects? And anyway pure functions are abstractions too 🙂 You just decide to ignore the side-effects (for very good reasons). We like to categorize things but in the end I think it's just about whether you work at the right level of abstraction or not.
d
Not exactly. composing map after fold and fold after map produce two very different results.
But in a purely declarative language, the order of expressions really doesn’t matter.
(Nor does the order of execution)
I may be wrong about order of expressions. But, most definitely, the order of execution never affects declarative languages.
d
The order in which you call map and fold "matters" not because of order of execution, but because of the dependency graph or structure. It's the same difference between an array of dictionaries and a dictionary of arrays
d
Pure declarative languages only really exist in mathematics. In math, you have the luxury of abstracting away everything that isn't relevant to the description of some mathematical object. The order of subexpressions in a declarative sentence only matters iff you are describing a mathematical object with ordered elements. Eg, you might be describing a sequence of events that occur over time.
Once you take a declarative language and make it executable, then it becomes a leaky abstraction. Once you have to worry about memory consumption and execution time, once you start changing a declarative description in order to optimize performance in the underlying execution engine, then the description is no longer purely declarative.
d
you might be describing a sequence of events that occur over time
You articulated precisely what I had a vague feeling about, that order may be an important language construct for efficiently communicating the idea, even if it doesn’t affect the final outcome. I’d like to point to Answer Set Programming as a pretty darn good counter example to the assertion that there are no declarative languages. The tech is still young, so some problems do require the optimization you’re describing, but many problems can be encoded completely naively, and termination is always guaranteed.
d
Music notation is an example of a declarative language that uses ordering of subexpressions to describe a time ordered series of events.
Regular expressions are a very successful example of a declarative language. Regular expressons are far simpler and more abstract than the equivalent imperative program working on an array of characters. In their original "pure" form, regular expressions were used in mathematics to describe sets of strings. Perl and Unix regular expressions are less declarative than their mathematical predecessors, because you need to understand how the execution engine works, you need to execute the regex in your head, in order to understand what substring will be captured by a \(...\) subexpression, for example.
a
Is there a connection between scope and imperativeness? The whole concept of introducing bindings into a scope that are accessible in subsequent statements, whether or not mutation or effects are allowed?
Or I guess you could say outcome or validity depending on lexical order?
If so, then musical notation is imperative for sure. Things like key signatures and dynamics could be said to be stored in scope as the piece is played
In a pure functional language without shadowing of identifiers, order is arguably a convention. You could choose to infer execution order. Shadowing is just syntactic sugar for choosing a new name.
d
@Alan Johnson In Haskell, the list expression [a,b,c] produces a different result than [c,b,a], because a list is an ordered sequence of values. So the outcome depends on lexical order. This doesn't make Haskell an imperative language. But Haskell has lazy evaluation, and the order in which the subexpressions a, b and c are evaluated depends upon context, unlike in an imperative language.
The essence of imperative programming is: Mutable variables that can be modified by assignment statements; Algorithms expressed by executing statements, which have side effects, in some order, using sequential, conditional and iterative control structures.
a
I guess I should specify that I mean the outcome of the elemental "sentences" of the language, not that it can define data structures with inherent order
d
@Alan Johnson said "Is there a connection between scope and imperativeness? The whole concept of introducing bindings into a scope that are accessible in subsequent statements, whether or not mutation or effects are allowed?" Scoped variables are a feature of every non-esoteric high level programming language. Even the lambda calculus has this. I would say no, you need mutable state in order to quality as an imperative language.
a
Prolog doesn't really have them, and I wouldn't say it's esoteric
For some reason, I think of the (simple) lambda calculus a bit differently from having scope, in the sense I'm thinking of. Maybe that's because it doesn't natively have statements
d
Prolog does have scoped variables, by my way of thinking. In this program: mother_child(trude, sally). father_child(tom, sally). father_child(tom, erica). father_child(mike, tom). sibling(X, Y) :- parent_child(Z, X), parent_child(Z, Y). parent_child(X, Y) :- father_child(X, Y). parent_child(X, Y) :- mother_child(X, Y). the X, Y and Z variables have local scope in each rule they occur in, analogous to function parameters in a functional language or in the lambda calculus. Lower cased variables like tom and sally have global scope.
@Alan Johnson The meaning of the word "variable" is different for imperative and functional programmers. I have a functional and declarative programming mindset, so for me, a variable is an immutable association between a name and a value, defined over some scope. I use the term "mutable variable" to refer to imperative variables. If I'm thinking formally, from the perspective of denotational semantics, then I think of an imperative variable as an immutable association between a name and the address of a mutable memory location. I've participated in forum threads where imperative programmers prefer to call functional variables "constants", and I notice that the Swift language documentation uses this terminology. (I think that's wrong: a constant is an expression which always has the same value, in every execution of a program. 42 is a constant.) So there is a cultural divide, where the two cultures use the same words differently. It appears we may have a different understanding of the word "scope", so maybe that's related?
a
yeah, i'm not counting parameters as scope. i think there needs to be some form of abstract reference to do anything interesting at all in a programming language
d
I’ve had a major paradigm shift in the last month or so. Before, I didn’t consider parameters as scope, nor closure as mutation. Now I’m starting to see that when a function rebinds its parameters, it’s modifying its environment - it’s mutating things. That’s why it’s possible to simulate mutable state with recursive functions. If you’re trying to avoid the complexity caused by mutable state, I think any parameter bound more than once needs to be considered mutation.
a
i think that's true in the sense that it's more complex than when there is no rebinding, it's still significantly less complex than allowing mutation that is visible through other copies of the reference