Last active
December 14, 2016 05:53
-
-
Save greggirwin/e8b2b3c9310df477d31529297292c2de to your computer and use it in GitHub Desktop.
A simple simulator thought experiment, using blocks as the main data structure
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Red [ | |
author: "Gregg Irwin" | |
] | |
comment { | |
From Gitter, @this-gavagai asked about modeling in Red compared to modeling | |
in traditional OOP languages. Here's the concrete example given: | |
" | |
Imagine you wanted to simulate economic dynamics in a third world village. You | |
might begin with 30-50 households, each with 2-12 people. Most of these people | |
would own a couple of different kinds of fields, and some of them might also own | |
cows. Cows produce milk, which is a good source of cash, but this production is | |
dependent on a number of factors including an animal's general health and well | |
being. Any model of animal health would almost certainly be influenced by (among | |
other things) what its owners choose to feed it, which in turn is going to be | |
influenced by both the costs of fodder in the village generally and specific | |
agricultural outputs of the household that owns it. | |
In other words, what I need to describe are templates for entities that can be | |
instantiated in relatively large numbers to behave quasi-autonomously in a time- | |
series simulation. The goal would be able to "experiment", testing for example | |
how the arrival of a veterinary clinic in town (or a new breed of cow with | |
different health functions, or a drop in fertilizer prices, or a sudden decrease | |
in rain) would influence the system's overall dynamics. | |
In an OOP paradigm, I would start by defining classes and their public | |
interfaces, and then I would arrange them into hierarchical graphs. Some of | |
these classes would be relatively self-contained (i.e., weather), some would | |
depend on a parent many-to-one (i.e., people in a household), and still others | |
would exist one-to-one as an extension of a parent (i.e., a particular cow's | |
health, which might be complex enough to benefit from encapsulation as an object | |
in its own right). That all works fine enough, but I'm not sure classic OOP is | |
actually the right model for any of it. The hard distinction most languages | |
expect between code and data, for example, is very limiting, and Rebol/Red's | |
approach seems to promise a much more elegant way of thinking about entities in | |
a simulation. The possibility that a vet might show up and re-write a particular | |
cow's food consumption behaviors at runtime, for example, is just way too cool | |
to ignore. It opens the door for unprecedented new kinds of elegance and | |
expressiveness in this kind of scientific modeling. | |
Right now, what I'm trying to figure out as I get more familiar with Red is just | |
general principles. For example, as in my past few posts, how should I think | |
about the choice between hierarchies of objects and complex blocks? I get how | |
both syntaxes work, but without a deeper intuition of why red programmers do | |
things the way they do I know I'm just going to fall back on whatever I do in | |
python. | |
" | |
The hard part about answering questions like this is that there is not one | |
"right way" in Red. Red is about letting you express the solution to your | |
problem however it is clearest to you and others working with it. This sketch | |
is me streaming out ideas. The approach here is very block based. It would be | |
instructive to do a comparable object based version, and also a hybrid approach. | |
Each has strengths and weaknesses. The best solution depends on the details. | |
Do you want to be able to persist simulations? How might you analyze the data? | |
Is there the risk of using untrusted data? What is the easiest way for end | |
users to create their own models? Do they have to understand the code, or are | |
files in a format they can edit easily? What comes to mind naturally in how | |
you *want* to express the solution? | |
} | |
;------------------------------------------------------------------------------- | |
e.g.: :comment | |
;------------------------------------------------------------------------------- | |
; First, what are we talking about, and how do we want to talk about it? | |
e.g. [ | |
[ | |
house ; 30-50 | |
people ; 2-12 | |
cows | |
fields | |
] | |
[ | |
[Bob buys 2 cows [bessie and flossie] from Carol] | |
[Alice moves to house #5] | |
[Dan buys field #37N...] ; (It's a UTM coordinate system, right? ;^) | |
] | |
; Can we have items with the same name, and so need namespaces? | |
world [ | |
cows [ | |
bessie [] | |
flossie [] | |
] | |
fields [ | |
flanders [] | |
flossie [] | |
] | |
] | |
; How do you define the starting point? | |
make-world [ | |
50 houses ; no spec means random mix | |
250 people ; no spec means random mix | |
500 fields [ | |
half small half large | |
30% oats 30% wheat 40% alfalfa | |
] | |
1000 cows [80% healthy] | |
] | |
] | |
;------------------------------------------------------------------------------- | |
; In our data model, we can embed relationships in the records themselves, | |
; or use "join tables". Each has pros and cons. We also have the option to | |
; key the blocks with an ID for each item, making selection easy, or making | |
; the ID for items a value in the block, making them self-contained; or both. | |
; A functional approach might embed the IDs, and use HOFs to traverse blocks. | |
; An imperative approach, or random selection of data by key, often makes | |
; for very "obvious" code. | |
; I'm Using all blocks here, rather than objects, just for this example. I | |
; did that because many new Reducers (Red-users) are familiar with OOP, but | |
; may not have seen this approach in other languages. | |
; | |
; A key point is that you `make` objects from prototypes, but objects are | |
; not automatically deep-copied (cloned) in Red. When using blocks, you can | |
; `copy` a block, which is similar to `make` on objects. To copy all nested | |
; series values you use `copy/deep`. | |
; | |
; You'll notice the _ values in the blocks below. They have no special | |
; meaning to Red. They are just a placeholder for a value. In many cases | |
; you'll use `none` in your Red code. The reason I didn't here, is because | |
; I expect to load and save simulation data, and (this is the important bit) | |
; you have to remember that everything in Red is data *until it is evaluated*. | |
; Why is this an issue for `none`? It just a matter of what's clear and easy | |
; to write, if you want a true `none!` value. | |
; | |
; red>> type? second [a none] | |
; == word! | |
; red>> type? second [a #[none]] | |
; == none! | |
; red>> type? second reduce ['a none] | |
; == none! | |
; red>> type? second load "a none" | |
; == word! | |
; | |
; I'll leave it as an exercise for the reader to `load` data from a file | |
; and experiment with evaluation. Sufficed to say that `_` in the blocks | |
; is just like `none` as far as Red is concered, *until it is evaluated*. | |
; Both are `word!` values, which you can think of as symbols in some other | |
; languages. | |
prototypes: [ | |
world [ | |
weather [] | |
houses [] | |
people [] | |
fields [] | |
cows [] | |
] | |
house [id _ size _ occupants []] | |
person [id _ house _ fields [] cows []] | |
field [id _ location _ size _ crop _ health _] | |
cow [id _ health _ weight _ age _] | |
; Relationships. These could be used instead of the 'occupants, | |
; 'fields, and 'cows sub-blocks in 'house and 'person respectively. | |
housing [] ; house-person pairs | |
ownership [] ; person-[cow | field] pairs | |
] | |
; What kind of helpers might we want? | |
crop-value: func [type][ | |
; We don't have a money! type in Red yet, so use ints for now. | |
select [oats 45 wheat 35 alfalfa 25] type | |
] | |
field-value: func [field][ | |
field/size * (field/health * 100) * crop-value field/crop | |
] | |
cow-value: func [cow][100 * cow/health] | |
house-max-occupancy: func [house][select [small 2 medium 6 large 12] house/size] | |
;------------------------------------------------------------------------------- | |
__WORLD: none ; Working target for making a world from a spec | |
; Note that we could use also COLLECT while parsing, to build up | |
; our world. More below. | |
; This little dialect isn't smart, but it implements an example world | |
; definition in the most basic way. This lets us play with the dialect | |
; from the user's side, very quickly; without being too strict in the | |
; definition (yet) to ensure things aren't redefined, or that values | |
; fall within constraints. | |
world=: [ | |
some [ | |
(set [count type spec] none) | |
set count integer! | |
set type ['houses | 'people | 'fields | 'cows] | |
opt [set spec block!] | |
(make-items count type spec) | |
] | |
] | |
make-world: func [spec [block!]][ | |
__WORLD: copy/deep prototypes/world | |
; If the world spec was valid, `parse` will return `true`, and we | |
; will return the world we built from the spec. If the parse fails, | |
; `if` returns `none`. Remember, Red functions return the result of | |
; the last expression evaluated, you don't have to use `return` at | |
; the end of funcs, only for early exits. | |
if parse spec world= [ | |
__WORLD | |
] | |
] | |
; This function deserves a little explanation, because someone will | |
; surely comment on it. Yes, it has side effects against a global data | |
; structure. If I were building this out, I might change that. On the | |
; other hand, it would depend on how big I thought the solution would | |
; ultimately be. Where a functional approach would either just build | |
; up the list of items and return it, for the caller to consume and add | |
; to the __WORLD, or it would take an accumulator (which I did on my | |
; first pass with this function), and add the results to that. | |
; Accumulators are a fine model, and I like them a lot, but eventually | |
; the results all need to go somewhere, right? And we sometimes just | |
; push the complexity around, or even add to it, by planning beyond our | |
; needs. It's a form of premature optimization, targeted at scope rather | |
; than performance. As an exercise, do it without side effects, and | |
; without an accumulator. If this function can be ignorant of how the | |
; world uses the result, that's good. e.g., it should return *just* a | |
; block of items, because it doesn't know if the rest of the system is | |
; going to use their ID as a key. Now our function is simple and likely | |
; idempotent. But what effects ripple out from that into the rest of | |
; the app? What else might you need to do? With no knowledge of the | |
; global structure, it can't safely generate IDs, right? Do we need to | |
; make it possible to call `make-items` multiple times for the same | |
; type? People will disagree with me on this, but I don't believe there | |
; is a one-size-fits-all paradigm in languages, any more than there is | |
; a single methodology that will work equally well for all teams and | |
; projects. | |
; It's absolutely true that our data structures and algorithms affect each | |
; other, but it does *not* mean ; that binding them tightly together, as | |
; in OOP, s always best. | |
make-items: function [ | |
count [integer!] "Should be > 0" | |
type [word!] "[houses people fields cows]" | |
spec [block! none!] "Not required, so may be none" | |
][ | |
key: map-type type ; convert plural to singular | |
proto: prototypes/:key ; find the prototype spec for this type | |
blk: __WORLD/:type ; find where items are in the world data | |
;TBD: This is where we would apply the spec as well, if one | |
; was given, passing information down to specific make* | |
; funcs for items, changing values based on percentages, | |
; etc. And each would have their own sub-dialects. | |
repeat i count [ | |
rec: copy/deep proto ; deep copy so records don't share sub-blocks | |
rec/id: make-id key i ; use singluar typename (key) for the id | |
repend blk [rec/id rec] ; add the ID as a key and the record as a sub-block | |
] | |
new-line/all blk on ; so each item is on its own line when molded | |
] | |
; I wrote the above, then needed to write the support funcs. As you | |
; might guess, what you see here is the result of a few refactoring | |
; passes. | |
make-id: func [type [word!] n][to word! append form type n] | |
plural-to-single: [houses house people person fields field cows cow] | |
single-to-plural: [house houses person people field fields cow cows] | |
plural?: func [type][make logic! find/skip plural-to-single type 2] | |
singular?: func [type][not plural? type] | |
map-type: function [type][ | |
; Clearer as two lines? | |
;map: either plural? type [plural-to-single][single-to-plural] | |
;map/:type | |
; Or one? | |
select either plural? type [plural-to-single][single-to-plural] type | |
] | |
;------------------------------------------------------------------------------- | |
; BIG CAPS for important global data structures, to make them | |
; stand out while drafting ideas. | |
STARTING_WORLD: make-world [ | |
50 houses ; no spec means random mix | |
250 people ; no spec means random mix | |
500 fields [ | |
half small half large | |
30% oats 30% wheat 40% alfalfa | |
] | |
1000 cows [80% healthy] | |
] | |
; At this point we have generated a world as a starting point. We can | |
; save that off, pre-generate lots of them, and apply different criteria | |
; for a simulation run against them. When we start a sim, the first | |
; thing we do is copy our starting world, then we can log each action | |
; and change that occurs, which is a running log that could be independently | |
; applied to different worlds. Obviously any named resources need to exist | |
; in the world, but you could, for example, start with cows or fields in | |
; varying states of health or have vets and horticulurists take different | |
; actions. | |
WORLD: copy/deep STARTING_WORLD | |
; Events that occur in the sim go here. | |
WORLD_CHANGES: copy [] | |
; Did it work? Do we have a world that matches our spec? | |
print mold world | |
;------------------------------------------------------------------------------- | |
; Note that by using keys for records, we can't seamlessly also use | |
; indexes into those blocks. You can still use `foreach [id rec] ...` | |
; to iterate, but if you want to say houses/1, or cows/5, eliminate | |
; the keys from the blocks. What I would probably do, though is write | |
; helper funcs to select items. (see below) | |
world/cows/cow1/health: 90% | |
print cow-value world/cows/cow1 | |
; Should we allow multi-select? | |
get-entity: function [type [word!] key [word! integer!]][ | |
if singular? type [type: map-type type] ; pluralize | |
; Double the index, if an integer, to account for keys in blocks | |
either word? key [world/:type/:key][pick world/:type (key * 2)] | |
] | |
;get-entity: function [type [word!] spec [word! block! integer!]][ | |
; if singular? type [type: map-type type] ; pluralize | |
; res: collect [ | |
; foreach key compose [(spec)][ | |
; ; Double the index, if an integer, to account for keys in blocks | |
; keep/only either word? key [world/:type/:key][pick world/:type (key * 2)] | |
; ] | |
; ] | |
; either 1 = length? res [first res][new-line/all res on] | |
;] | |
probe get-entity 'cow 1 | |
probe get-entity 'houses 'house2 | |
probe get-entity 'person 5 | |
probe get-entity 'field 'field3 | |
;probe get-entity 'people [person3 5] ; requires multi-select version above | |
; We can use lit-word! params in the function def, so we | |
; don't have to use lit-word! args in the call. | |
cow: func ['id][get-entity 'cows id] | |
probe cow cow3 | |
; From here we could write a top-level simulator dialect, wrapping all the | |
; commands and querys we need. | |
;------------------------------------------------------------------------------- | |
; I'll stop here for now. This was a fun project to think about, and I've | |
; given you a lot of information and open-ended questions and experiments | |
; to do on your own. | |
; | |
; Remember that this isn't a finished, planned design. This essay is a | |
; first step in exploring the problem space and discussing alternatives | |
; with you. It's not an attempt at literate programming, but an example | |
; of how I use Red as an "interactive" design tool. A language for thinking | |
; about problems and their solutions. Being able to express and *see* what | |
; we're talking about is a big part of Red's power. | |
; Happy Reducing! | |
halt | |
; P.S. Yes, you can paste this whole thing into the Red console. | |
; P.P.S. The shell console has a paste issue right now, so you can only | |
; paste this into the GUI console and have it work. Otherwise, | |
; run as a regular script. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment