If anyone wants to join today at 10:30 am MDT, I’ll be hosting a discussion group for two pieces by Brett Victor, Learnable Programming and Up and Down the Ladder of Abstraction. https://www.guidedtrack.com/programs/og010mb/run
If someone were to visualize data flow through time for Clojure, how would it do that differently than Factorio or Opus Magnum? https://www.youtube.com/watch?v=U-aXBcJfxM0
Not sure I 100% understand your question, but you bring up an interesting subject. Whenever I build systems with queues I feel like I'm playing factorio Is this question related to your previous posts regarding visualization?
Yeah. Factorio and Opus Magnum are models of imperative programming, so I didn’t know what it would look like if it were functional
I'm not sure I'd say factorio is imperative. Can's assemblers be compared to pure functions? What's the difference between chaining assemblers and composing functions?
The input is destroyed once it's fed into an assembler. You can't direct the same exact input into multiple assemblers, the model just doesn't work.
So they aren't even suitable for imperative programming. You can jump through some hoops and build some resemblance of a model, but I don't see how it could be useful in those particular games.
message passing? :thinking_face:
So some functions would need to just observe the outputs of other functions and not have it physically transported to them?
No idea how to interpret the questions. FWIW, and not to be a mood killer, but I sincerely doubt it's possible to make a visual representation of a working program in a useful and flexible way. You can visualize some simple cases, sure. In fact, that's exactly what's going on in some visual programming environments. But you can't do it for a general case - as soon as interop is involved, all will break. Or data mutation. Or higher order functions. Or macros. Or dynamic binding.
You can visualise pure functional programming by looking at the types of the terms. For example in Haskell, you could graphically represent the whole program by representing its types. And the pipelines are achieved by connecting one type to the other.
But that is only possible due to the fact that the terms are statically typed and the functions are pure. Unsure, if that can be achieved in Clojure due to the way of handling side-effects and dynamic typing.
Even without dynamic typing - how would that work when most of the things are maps/vectors/lists/sets?
All data structures have an associated type. So you would know what type comes in and what type comes out
Depends at what level of typing we are talking. You can achieve fully typed terms (where the type depends on the contents) by working with dependent types.
a la Idris or Haskell with extensions.
Consider this function:
(defn add-field-to-all [k v ms]
(into (empty ms)
(map #(assoc % k v))
ms))
Does what you describe still apply here, assuming the function is used in 15 different contexts where it's unknown in advance what shape each map has?So the type of a map in Haskell is Map a b
where a
is a type variable for the key and b
is the type variable for the value. I expect your function’s type signature to be add-field-to-all :: a -> b -> Map a b -> Map a b
. As long as the types of the key and value match the type of the map, there is no issue
where the last Map
is what is returned - it is just currying notation.
Now I'm even more confused. So here you are not talking about dependent types and just the real (type x)
thing. I'll skip the fact that a -> b -> Map a b
is just wrong for the reason you yourself mention.
How can knowing (type x)
possibly be useful for pure FP visualization?
unsure what is wrong about it
and the type itself is the visualisation
and yes, you are correct, these are not dependent types.
the type notation is correct and the way it is written shows you what is the next type that you get by using currying and partial application.
Map
is a type constructor that takes two type variables a b
and returns you the type Map a b
but a b
can be any type
> unsure what is wrong about it
Maps are heterogeneous. {1 2 :a "c" "x" 1.5}
> the type itself is the visualisation
I might be misinterpreting but I think the OP wants to have a visualization of a whole program or at least an algorithm, in action. Or, well, in simulated action.
Having just a type spelled out is orthogonal IMO.
A type can have multiple constructors Data MapContent = I Int | S String | …
so you can have Map String MapContent
which gives you your ability to mix the contents of your type.
A program in Haskell is a composition of types - which you know at compilation time as they are statically declared or inferred. If you write out or graphically represent each type, then you have a representation of the program.
you can even go to the extent where your map is Map MapContent MapContent
and you can have your MapContent
to be a recursive type where you can recursively construct your term
But when you deal with user data, you can't know the type in advance, unless it's some sort of Any
, no?
Well you can. Because you define it in advance what it is, or what it can and cannot be.
> If you write out or graphically represent each type, then you have a representation of the program
So suppose I have Map Any Any
. And Vector Any
. I'll drop Any
for brevity.
And also assoc :: Map -> Any -> Any -> Map
and conj :: Vector -> Any -> Vector
.
And my program is (assoc {} 1 (conj ["input"] *input*))
.
How exactly do the types written out above constitute a representation of that program?
For example a JSON object which can have a heterogeneous structure could be typed like this:
data Value = Ob Map String Value
| Ar [Value]
| St String
| In Int
| Fl Float
| Bo Bool
| Null
Yes, and it's a text representation of a type. Not a graphical representation of a program.
with that type, you can have a function f : Value -> Value
which takes a Value and returns a Value
that is the representation of your program
say you actually want to compose that with another function g : Value -> Int -> Bool
g . f : Value -> Int -> Bool
And so on
I'm still not sure what I'm supposed to get from those types.
OP asks not about a representation, but specifically about graphical (I assume so given the examples) visualization of data flow.
Having f : Value -> Value
doesn't show me where f
is called. Doesn't let me intuit how often it's called. It's just a statement that f
exists with such types.
Speaking in terms of Factorio, there's a difference between an assembler with a recipe (which is just like f : Value -> Value
) and an assembler with a recipe that's part of a larger pipeline.
Well, if your program is made from typed components, then you can literally represent their types - graphically as I have shown you. Because you know from the get go where the program enters - let’s say it is function main
. Then all you need to do is represent all the functions that get called when main is executed. Which can either be
main :: Value -> Int -> Bool
or it can be main :: g . f
etc. (or infinitely many functions composed). The one thing you would be unable to represent is what happens inside the type itself (i.e. what happens to the Int
once it enters the function) all you know it enters as Int
and comes out as something else Value
for example.
If you are interested, or curious https://bartoszmilewski.com/2021/02/16/functorio/ wrote an awesome article about this.
It all, including what's described in the article, still describes write-time and compile-time. Not run-time, which is the crux of the thread. With a proper visualization tool, I would like to see a queue being clogged. With type notations I will only see that a queue might exist in runtime - even its existence is not guaranteed.
But I'll let Rob be the judge of whether my assumptions are correct or not.
Related to the topic in general - I mentioned visual programming at the start. Here's a demonstration of how something similar is done in Unreal Engine Blueprints: https://youtu.be/vCmxwyw6lkY?t=98
Ok, no worries. I am willing to accept my opinion might be wrong or not fit for purpose. Was proposing a different perspective on the matter, and maybe trying to highlight the power of insight that comes with pure functional programming with types. Some things are undecidable, so you cannot describe them with types - like a clogged queue, but the handling of such a situation can be.