Skip to content

Latest commit

 

History

History
349 lines (261 loc) · 48.9 KB

File metadata and controls

349 lines (261 loc) · 48.9 KB

comparing programming languages

Table 1. some langs
lang runtime runtime efficiency freedom syntax simplicity special shit

java

VM

good

no

v.bad

no

oop typing

clojure

I

bad

ok

bad

???

???

c

N

good

low

medium

high

no

haskell

VM

bad

low

ok

good

typing, referential transparency

rktscm

A

bad

ok

bad

good

no

lua

I

bad

low

bad

high

no

j

I

good

low

low

good

array model: shapes, and boxes

k

I

good

???

low

high

no

factor

A

sus

v.high

no

high

no

  • also rust, go, zig, odin, futhark.

  • btw, (better) alternatives to haskell: ocaml, erlang, pony

runtimes: VM (compiled), I for interpreted, N for compiled-native, A for ambivalent to compilation or interpretation, JIT for just in time compilation.

notice that, despite racket scheme being a simple model and syntax, i gave it only a "good" simplicity score because it’s so flexible that there’s no de facto way of coding. also, conceputually similar codes are often incompatible because of type differences e.g. streams & sequences. this problem does not exist in picolisp.

  • scheme is better haskell: it’s differences are: unified list & tuple; untyped; better metaprogramming. scheme is a smaller spec, more flexible. its lack of a type system demonstrates that the type system, while helpful, should be optional, because it’s often unnecessary and so combersome, or even limiting!

  • lua is like c but with good unicode support,…​and indexing that starts at 1. that ends-up killing any hackiness. lua is both interpreted and small, which are nice. worse syntax than c. built-in support for dicts is good, as is the fact that its vectors are just dicts with implicit indices.

apls have unique properties: terseness, inlined code rather than function definitions, and truly one data structure. factor & scheme are powerful by their generality and few, simple primitives; but that much power is unnecessary, and that much generality is undirected, unsuggestive, and verbose. to help with these, people create data & syntax structures, but that’s ad-hoc and immediately gets away from the very simplicity & generality of scheme or factor themselves! not only that, but it’s ad-hoc; it’s obviously untennable to try to create structures so that they all work together well, or that they support all the functionalities that anyone would want. the generality is so great that it doesn’t converge well. if one’s judicious, then they can avoid this, and actually use only one structure, but that structure is necessarily k’s vectors/maps/tables, sql’s relations, prolog’s predicates, because it’s the theoretical smallest structure: the n-tuple; data exists, and each datum belongs to some sets; subsetting is structure exactly.

thus ultimately k, factor, prolog are all the best, and neither is ideal. ideally we’d mix them altogether to be terse, concatenative, use predicates alone for structure, and be homoiconic so that we can define specific syntaxes (as is rarely necessary) for things such as dataflow programming. summary of ideals:

  1. code directly reflects your thought. ideal code is to code the contents of your mind, verbatim. the argument against this—that some proglangs have you manipulate data better than you’d do in your mind—is misguided: it’s not a question of model, not code. code, like all language, can be stylistic, suggestive. reduce the amount of transcoding between your mind and whatever persistent medium your computer supports.

  2. our coding system is completely general, homoiconic/metaprogrammable/self-modifiable statically and/or dynamically; this enables us the greatest freedom in coding

  3. relations/constraints described clearly in terms of a/symmetries is the only structure, since constraint is the only thing that defines programs. relations compose flatly, and more easily than functions, too, which is appropriate & nice. constraints compose by AND & OR, which are (for referentially transparent code) commutative, whereas functions are generally non-commutative, so function tools, considerations, syntaxes, all reflect this, such as by needing to nest function calls within each other. concatenative langs do not nest, happily, but generally their order matters.

    1. specifying a/symmetries is naturally declarative. programs are well-divided into two partitions: rules, and sequences of transformations. combining these is typically the difficult aspect of coding, since most coding systems don’t support the pattern "x, unless y in which case z" since (y,z) is after the fact of x. the control flow for expressing that is clumsy or ugly; solutions that you see are if statements oddly weaved-in all over the place, or the "callback function" (aka "hook") pattern, or exception handling. it’s fine in prolog because prolog’s control flow is implicit, built on rules, and the assumption that they’ll fail, in which case backtracking & trying new rules is done (and also that looping is implicit b/c each symbol represents a set, and a collection of symbols represents their cartesian product.) the catch is that in prolog one must use this system for dataflow, too, which is very common, and ugly by this system. most langs have a plethora of control flow devices that must be explicitly spliced into the program where it’s used, in every place where it’s used. one may naively say, "just factor it into a "checking function" then put that function wherever it’s used" except that functions themselves have a very specific control flow, and even in such general languages as scheme or factor, writing such functions such that they’re elegant, is not truly impossible. your best bet is to try to fashon some macros around continuations (which are first-class), but that never goes well. a picture of it going well invariably approaches prolog, so again, it’s best to have some mix of prolog and a dataflow lang.

i like aaron hsu’s "readability" metric: the amount of money that you’re willing to bet that a change you make to the code will work the first time without breaking anything nor first testing it mechanically. "this removes the ability to use a mechanical proof theorem system, a mechanical type system—none of that. it’s just you, your code, a cup of coffee, and lots of money." and isn’t that last sentence the dream, the grand aspiration of all real programmers? "confidence, not comfort."

concatenativity’s supremacy

to be precise, it’s not strictly concatenativity: it’s generally the ability to arbitrarily partition code—a sort of "continuous modularity". this property equals concatenativity for linear (unidimensional), sequential programs. for database paradigms like prolog, which don’t have a dimension except for size, the fact of all predicates being defined independently and staying independent, their on-the-fly structuring when given a query, often being non-deterministic, is just as desirable as concatenativity.

TODO: what’s the implication / relationship between concatenativity and pure tacicity?

after you go cat, you don’t go back. the most important thing is concatenativity, which is afforded by being tacit. variable names are just another thing that isn’t actual program logic; they are meaningless, used only to relate things, as a programming primitive. instead, do like factor: define a set of tacit primitve relations (these are the shuffle words in factor), then compose them. this way each word has definite meaning, and the composition of simple meaningful things makes complex meaningful things, but no matter what arbitrary subest of a program you select, you’re guaranteed that it’ll be meaningful! this works for any composition system, and any variety of primitve relation; they may be multidimensional, about tables, graphs, stacks, queues, or anything. of course, predicates or graphs are generally best, but it may be fun to experiment with other relation systems. with multidimensional program codings & editors, we could have some very good program code.

  • function inputs aren’t named, so refactoring is easy, and there are no scope concerns. without variable names, there is no scope, no shadowing concerns.

  • you don’t need to figure-out a whole program at once. you can start with what you know, then incrementially compose programs, and doing so is not cumbersome, since there’s no renaming to do nor syntax to fiddle with

    • any applicative code that you look at, you must, for every variable encountered, wonder how, if at all, it’s used in any of the upcoming code. this is less true if you use the let syntax of haskell, scheme, rust, &c, but even then, within those clearly-scoped blocks, you have no idea how the variables relate to each other! you don’t know when to stop caring about a variable and to consider that part of the function understood before moving on to a different part of the function! contrast this with dup, which tells you immediately and obviously, "we’re leaving that on the stack for later and starting a new computation right now, so you can forget about that value until we’re done considering the upcoming computation." see below for example.

  • debugging is simple because programs simply evaluate from left to right, and the only state that you must track is the data stack, and sometimes the retain stack, or dynamic variables. either way, it’s very neat, clean, simple.

  • because words are so simply defined (as sequences of other words), the language is likely to have metaprogramming features, since implementing them is easy. this is what enables, in factor: macros, the ability to modify quoted programs as lists, introspection, redefinition.

  • for stacklangs, reading multiple inputs and returning multiple outputs are easy

  • both refactorability and the ability to compose & split (henceforth "splice") programs are important! that we can, in catlangs, splice code freely with certainty that the parts being spliced remain independent is too good to ever not have. being used to it, the idea that changing some code would cause other code to break is ridiculous! i.e. in catlangs, splicing doesn’t entail resolving emergent namespace conflicts. no arbitrary subprogram should affect any other arbitrary subprogram!

    • for example, consider the nested j/k λ’s namespace problem. it doesn’t exist in factor, for any arbitrary number of compositions, since composition is just concatenation, and no part of a factor program affects others. in non-tacit langs, the very fact of a program growing is troublesome! that’s a glaring design flaw!

  • implicit currying: y f is equivalent to [ f ] curry call.

real-world examples of reading applicative code

since i don’t have any applicative code of my own, i went and got some from some projects.

bluez/src/adapter.c
static void set_exp_debug_complete(uint8_t status, uint16_t len,
					const void *param, void *user_data)
{
	if (status != 0)
		error("Set Experimental Debug failed with status 0x%02x (%s)",
						status, mgmt_errstr(status));
	else
		DBG("Experimental Debug successfully set");
}

so how are you going to read this? you have 4 inputs. turns-out that only status is actually referenced in the function body. you wouldn’t know that until you read through the entire function body! so what would you try to do in the general case? would you accumulate variables as they’re introduced, always looking for when they’re used, then try to relate it all? or would you ignore them and read through the code, looking-up each unknown symbol as you encounter them? that’s a much more practical method, but then you’ll be tracing through all of the prior code to build-up the symbol’s current value, possibly tracing through state, or shadowing [scope], or in the simplest case, you’ll have to search back to see where it was introduced!

here’s the factor translation:

: set_exp_debug_complete ( status len param user_data -- )
  3drop dup
  [ dup mgmt_errstr "Set Experimental Debug failed with status 0x%02x (%s)" sprintf error ]
  [ "Experimental Debug successfully set" DBG ] if-zero ; static
  1. immediately, at 3drop you know that you don’t care about those variables. thus you’re considering status (since it’s the only thing on the stack!) or you’re about to put something new on the stack

  2. dup means that we’re doing something with it while preserving it on the stack. dup before a conditional is common.

  3. the rest is self-explanatory. static isn’t a word in factor, but in factor, any adornments for the compiler follow word definitions.

rusty forecast’s weather.rs
fn fetch_weather_data() -> Result<WeatherResponse, Box<dyn std::error::Error>> {

    let city_name = read_city_name()?;
    let unit_value = read_unit()?;
    let unit_type = if unit_value == "C" {
        "metric"
    } else {
        "imperial"
    };

    let url = format!(
        "http://api.openweathermap.org/data/2.5/weather?q={}&appid={}&units={}",
        city_name, API_KEY, unit_type
    );

    let response: serde_json::Value = reqwest::blocking::get(&url)?.json()?;
    if response["cod"] != 200 {
        return Err(format!("Error: {}", response["message"]).into());
    }
    serde_json::from_value(response).map_err(Into::into)
}

firstly, what the hell is the author thinking with all this whitespace? very little being said here despite the amount of space it takes. sooo i see that we’re letting many things be. we have a city name, unit value, unit type, a url, …​ok, so at this point i’m already thinking, "so what are we actually doing? i see that we have these things, but i can’t appreciate them because nothing’s been said about them yet." keep in mind that for each variable encountered, i must look to see which of the prior-encountered variables its definiton includes. it turns-out that url is the first whose definition entails prior-bound variables. as it also turns-out, city_name and unit_type are used _only in defining url, and unit_value is used only in defining unit_type unit_value is near unit_type in source, but city_name_ is very distant from its use in `url. it’d have been nicer if it were actually used near url`. all the `let’s are pure, except for `response, which is attained through i/o. it’d be nice if the syntax made obvious which things were pure or not!

granted, this code could be styled better. this is the author’s fault, not rust’s. yet the author chose to code this way; somehow somethings ultimately suggested this style, and rust enabled it pretty easily. this style is not uncommon across applicative languages! i wonder why people choose to bind to variables rather than inlining their definiting expressions, and putting comments next to them to denote what concept their code represents.

anyway, the factor translation, written in the way that a factor user would write it:

: fetch_weather_data ( -- x )
  read_city_name
  API_KEY
  read_unit "C" = "metric" "imperial" ?
  "http://api.openweathermap.org/data/2.5/weather?q={}&appid={}&units={}"
  format! reqwest::blocking::get json
  dup "cod" at 200 =
  [ "message" at "Error: {}" swap format! into Err ]
  [ serde_json::from_value Into::into map_err ] ! i assume `obj.method(params...)` syntax to be like lua or python: syntactic sugar for method(obj,params...)
  if ! we don't use "return" in factor, so i use 2-way `if`. i could have thrown an error, though, effectively returning the error.

see, in factor one practically must introduce things into the stack immediately before their use, yet makes code more readable; factor practically forces its coder to write readable code! one is very strongly behooved to keep items on the stack for the shortest time possible, and keep the stack short, which means that both the reader & author don’t need to consider many variables simultaneously; reading factor code is a piecewise and fluid process. once something is put on the stack (i.e. once one reads the code, since factor is homoiconic), the reader expects it to be used very soon; or if it’s not, then they expect that it plus some following few things will be used altogether. these are reasonable expectations and make reading factor code wonderfully predictable.

how i read this factor code:

: fetch_weather_data ( -- x )
  read_city_name                                     ! thing. to understand the code as i'm reading it, i must know that read_city_name has effect ( -- x ).
  API_KEY                                            ! thing (constant).
  read_unit "C" = "metric" "imperial" ?              ! thing as other thing (unit as metric or imperial based on equality with "C").
  "http://api.openweathermap.org/data/2.5/weather?q={}&appid={}&units={}" ! thing (constant).
  format! reqwest::blocking::get json                ! format! is effectful; by its nature, i must look at its format string to know
                                                     ! which things are taken off the stack. i wouldn't be surprised to find that format!
                                                     ! consumes the whole stack thus far, though. and indeed, it is so.
                                                     ! ok, then we request from that obviously-url string then get json from it.
  dup "cod" at 200 =                                 ! dup soon followed by predicate, so this dup is probably for an upcoming `if`; thus each branch has
                                                     ! effect ( x -- ..b ). in fact, because `if` is the last word of this definition, i know ..b = x.
  [ serde_json::from_value Into::into map_err ]      ! idk what this means beyond, "get some value of the json, then 'map_err' it in an 'into' way."
                                                     ! i do know that i see `Into::into map_err` as one item, though; it's `map_err` parameterized by
                                                     ! a literal, like how i see `10 log` as "base 10 log". it may as well be one unary, curried function.
  [ "message" at "Error: {}" swap format! into Err ] ! i assume `obj.method(params...)` syntax to be like lua or python: syntactic sugar for method(obj,params...)
  if                                                 ! we don't use "return" in factor, so i use 2-way `if`. i could have thrown an error, though, effectively returning the error.

i also see the leading literal format string as a parameter of format! separately from format!'s arguments on the stack.

i found some buggy openzfs c code. the bug is seen as variables being unused. this cannot be a problem in stack langs; if data is produced, then it’s stored on the stack, and it must be consumed else you have stack effect error. a beauty of the stack is that it makes defining/holding too many data obviously bad, whereas it’s not as obvious in applicative languages.

conclusion

totally tacit is a blessing! use/make combinators & quotation rather than shuffling. and yes, arg ord is an important part of tacit program design, just like it is in haskell! although, factor’s swap is much easier to reason about than haskell’s flip! this fact generalizes.

one thing about applicative coding—namely declaring variables: it gives a place to specify the datum’s type/size. factor has declare which sucks, though using it everywhere might be still better than applicative code, but it isn’t great, either way. idk how forth handles typing yet.

lessons:

  • demand of your language:

    • mini

      • efficient

      • simple implementation

      • concatenative; binding to variables and scoping is just stupid:

        • makes metaprogramming a bitch (e.g. macro hygeine)

        • bloats your code with binding & scoping syntax (let …​ in …​)

        • forces you to specify variable names all over the place

        • prevents function composition from being implicit, so you must either use a composition operator (haskell h.g.f or j h@g@f) or stick an argument into the first function (haskell h.g.f$y or j h g f y), which is asymmetric

        • makes refactoring awful

      • simple language/computation model

      • minimal (number of rules) & terse (number of encoded symbols) syntax. should be natural if the language model is simple.

      • symmetric syntax

        • no operator precedence

    • flexible

    • interpreted. compilation optional.

    • dynamic

      • makes metaprogramming equal programming. factor is perfect example: all quotations are lists of words, which always have obvious definition because there’s no scoping / local variables, so subprograms are created, modified, and applied all over the place. lambdas and programs are equivalent in factor. this makes cond nothing more than a list of literals that we traverse using find, then evaluate using call.

      • playing with your living program is a joy and natural way to play with and explore things, and programs are no exception. you should be able to change your program as it’s running. this makes debugging easy. it can even be useful in the program’s normal course, such as modifying a server while it’s running.

    • good builtin unicode support

    • virtual sequences or virtual operations e.g. factor’s sequences: <zipped>, <reversed>, <iota>, &c.

      • sequences should implicitly virtually be dicts

    • easily transmutable data structures & flows. this doesn’t necessarily mean "untyped" or few structures, though those are correlated conditions; for example, factor’s type system, despite being nominal, is beautifully flexible, and there’s no unnecessary code that converts among types. "converting to the <reversed> type" is a necessary conversion because it’s equivalent to performing the reverse operation and is the same amount of syntax to do so (each is one word.)

    • (efficiently) mutable data structures. haskell and scheme are terrible for this; their linked lists cannot be modified easily. ideally one can specify a map of indices to functions, and apply that to an indxed structure to update it. given how easy that is, we shouldn’t settle for less!

  • indexing from 1 is proof that satan is alive & well today

  • it’s a language’s perogative to enable the programmer to relate & manipulate information, and the programmer’s perogative to use the language sensibly, correctly, responsibly. so don’t settle for a language that imposes constraints that aren’t implied by the language’s design itself; similarly, never use an overspecified language!

    • if you want correctness, choose convention, not rules. it helps code be mnemonic anyway. the goal is to prevent mistakes, not make them impossible. we want accident prevention, not making "incorrectness" impossible. "correct" may, in any occasion, change. there are exceptions to every rule. rather than designing "robust systems with escape hatches", design systems where costly mistakes are hard to accidentally do, and uncostly mistakes are easy to spot in code or as the program runs.

    • don’t allow yourself to be constrained to referential transparency unless it earns you appreciable parallelism at no-to-little extra cost.

    • (mandatory) (nominal) type systems are 100% pure, uncut ass. just say no. you can implement your own type systems or other constraints/checks easily, so diy or get a separate package/module for it.

    • even factor’s stack checker, which is usually good, prevents us from using each to modify the stack, which is a pretty basic & common need; to effectively do this, we must be verbose or hack around it.

    • scopes are implicit indexing forced upon the programmer by the language model. scope is a stack of maps from symbols to values/addresses. rather than the user choosing which map to select from, they’re forced into using some given map.

after using factor (stack lang), applicative programming feels like stringing countless wires from functions' output nodes to other functions' input nodes. if that isn’t spaghetti programming then i don’t know what is. by contrast, factor feels like the incremental modification that it is. no wires in factor—only code blocks that can be freely rearranged.

factor is just a better version of scheme. it’s the same thing but actually done well: effortless object transmutation, virtual sequences, &al miscellany, and the simultaneous elimination of parentheses and tacit function composition.

we know the phrase "no stinkin' loops." sure. true, even in haskell and scheme we find ourselves writing manual loops for functionality or efficiency. in factor this is very rare since factor has virtual sequences and efficient, mutable vectors, hashtables, etc. ofc in factor we use map &c. using haskell or scheme, if you’re avoiding mutation, then you’re greatly encumbered and may have to use manual loops just to decently-elegantly code state updates. rather than "loops" stinking, it’s really syntax about them that sucks, so we see that it’s actually syntax in general that sucks—nothing to do with loops themselves. obviously forths & apls are low-syntax, regardless of how "terse-in-chars" they are. even new langs that are to replace c in all or many cases, such as go, v, zig, rust, have even more syntax than c. has something so basic not been learned already? forth, lisp, and apl are the oldest langs, have been used in such amazing places as outer space and financial institutions, yet…​even in the 2010’s—40 years later—people are repeating algol’s mistake.

other considerations
  • safety, such as correctness or memory safety

  • parallelism

  • concurrency

ideal lang (design & implementation)

tl;dr: "'don’t try to design the code; that’s…​impossible. instead, try to realize the truth.' 'what truth?' 'there is no design. then you’ll see that good code merely describes your thought directly.'". you must model the situation elegantly, but that’s to be done in your own mind, regardless of whether you code it. hopefully that model is already available as executable code, but if not, then code it.

cat (maybe) w/debugger, smol codebase, efficient, terse, overloaded, good prim structs & ops, no import, interpreted w/optional compilation.

TODO: * how prolog & haskell differ? * sketch what tacit prolog would look like. tacitity is only a notational difference, but does require non-parameterized relational primitives rather than prolog’s single relational primitive: predicates' parameter vectors.

concatenativity assumes that programs are ordered, that they execute in sequence. one might suggest that this makes it incompatible with prolog, since prolog programs are sets of facts & queries. however, aspects of even prolog are ordered: 1. predicate arguments; 2. clause parsing & evaluation. the latter applies to any text language. functions generally relate, and the stack is a method of composing functions i.e. composing relations. while the stack is nice, it’s really tacitity that helps; tacitity directly reflects that variables are not the primitive program elements, but that relations are, which is appropriate because relations actually have meaning, whereas variables do not. rather, variables' meaning is only in terms of [relative to] other variables.

therefore we can generalize prolog and stack or concatenative models into a single type: Relation(…​) =: PrimRel1 | …​ | Relation(…​), which is symmetric and obviously enables metaprogramming. there are two aspects to this model: specifying and evaluating relations. concatenativity/tacitity makes code visually simpler & prettier, and easier to write & refactor. there may be a system that we use, such as the stack, to describe relations; this code may be literally followed by a compiler to construct a composite relation which will actually be used for computation in the executable (as compilers always do.) for example, a stack may be used by humans to describe a program (relations) and by the compiler to construct a db of relations, but the actual compiled program might not emulate a stack machine at all. it’s the compiler’s responsibility to convert code that’s easy & fast for humans into code that’s easy & fast for its target architecture.

  1. the simplest relation is a collection of things belonging to a common set, which can be represented by phenomena (audio, graphics, etc) sharing a common pattern.

  2. sequences have been and are yet the natural relation for computers since data is stored as byte sequences. programs have been stored as text, too, and text is unidirectional. i may create a general graphical representation/syntax of programs that is multidimensional, in which case sequenced items would need to match a common pattern (to represent that they’re of a common sequence) but also each element must match a pattern that orders it relative to its pred and/or succ. the simplest sequence is 2-element. data may belong to multiple sets or sequences. the following is an example of elements belonging to a common set (denoted by capitalization) and sequence (denoted by common row or column):

5 seqs, 2 sets
    h
    G
    f
A B c d E
    o i
      j k L m n

because we’re still using typewriter-based computing, where code is parsed as character sequences, you don’t see such syntaxes. we have neither the ui nor display for it yet, but it would be easier to make than a video game, so let’s get on it. at least we have prettyprint trees, but that display is formatted character sequences, so it’s really characters that present like a tree rather than a tree proper. therefore if we want to code as trees, then we must use text alignment tools like special text editor commands, and we must write special parsers that parse text representing a tree into an actual tree.

  1. sequences are virtually maps from natural number indices to elements.

  2. functions are virtual maps; both have dom & cod. e.g. 4 + virtually represents the infinite-cardinality map. technically, functions are stupid; multifunctions are actually reasonable.

  3. but even multifunctions are stupid; they’re unidirectional. why have a direction at all? what if we just look at the dual morphism? enter relations. they’re exactly the same as functions except with or preserving duality. functions limit functionality; use relations.

so we, abstractly, necessarily have sets. practically, we necessarily have sequences. and implicitly, we necessarily have maps. all of these can be virtual. so aside from arithmetic, what primitives do we need?

TODO: i need to decide distinct terms for predicate/relation (prolog), relation (sql), predicate (fn, typically to bool).

dynamic eval

in j, wrapping $: in a gerund, then naming then using the gerund in a sentence, substitutes the name by the definition, inlining it at the call site; then when it’s evaluated, $: is evaluated in that context; and indeed, it has sensible meaning only in that context.

generalized combinators

we can factor {x+2*y} & {y+2*x} into either and optional selfie. this seems different from usual factoring; it’s more than just identifying common symbols: it’s identifying common information.

primitives

rather than having primitives, programs are entirely just predicates. this is the logical & programmatic primitive construct, and is the only aspect of the language aside from any syntax(es). there’s no need for "primitives" per se, though naturally we’ll, for convenience/functionality/structure/design define some predicates that will be more frequently used than others. all optimizations will be added as rules. all programs are defined like factor’s multiple dispatch: a collection of unordered ad-hoc rules. rules define functionality/relation, including fusion and optimizations; because all predicates/vseqs are composable (they’re rules, and sets of rules support set union), effectively all programs are adverbs [j,k] and special cases may be defined (such as how maximum has multiple definitions in factor e.g. i can specify that the fold >/, when predicated over the "iota" virtual sequence, is just the iota’s count argument minus 1. because predicate argument vectors are totally ordered by specificity, the order in which predicates are defined is irrelevant; the more-specific ones are always tried before more general ones.

TODO: ensure that i’ve already discussed the virtual sequence by this point. vseq represents set, relation, seq, fn b/c sets are primitive, seqs are indexed sets, and relations generalize both. predicates are how we specify vseqs. they’re how we express the subset identity & relation together, like in sql: select <expr> from <vseq> where <pred>, or in haskell’s list notation: [expr←vseq,pred]. we can do like a mix of sql & k: have a lambda literal whose namespace includes whatever attributes that the argument vseq exposes: {expr,pred}[s1;s2]. certain predicates will effectively do refinement typing but better e.g. the earliest(idx,vseq) predicate will check to see if vseq is sorted by idx; if sorted ascending/descending then take at smallest/largest idx; and if not sorted then fold through vseq to get the element having the least idx. remember that idx is just one arbitrary attribute of this relation [sql]. rather than type checking, we’ll have algebraic predicate unification based on the only two primitives: set membership & order. (sub)programs will specify which arguments of their predicates must come in ordered or leave ordered; this way ordering is done as necessary by the computer, not something needed to be tracked by a programmer. some other properties that the user can specify as sub-predicates: size change (a natural number), order preservation (boolean. subseq is an example), set preservation (boolean. permute is an example), ordered (asc,desc,f). make everything as implicit as possible, so that the predicates can imply operations as much as possible, so that the programmer only ever specifies what they what their program to be, not what it must be in order for their desire to be fulfilled! thus it’ll be prolog but with better syntax, more algebraic, and with vseq primitives rather than merely predicates and linked lists (and linked lists are horrible). rather than force the user into true logical primitives, we’ll allow the user to define their own algebras/axioms, specify whether a subprogram obeys any axioms, take their word for it, leverage axiom-implied optimizations or thereby deduce dataflows to get the desired result, and use the sensible vseq primitive rather than treating lists and predicates separately!

vseqs are defined per rule e.g. x[5]=6 just implicitly "created" a vseq called x whose value at 5 is 6. i can then add another rule: x[y,z]=[y%z]. now there’s another rule for x which uses variables y & z. the namespaces inside the index operator (brackets) is as it usually is for fns; symbols are bound on the left side of = and thereby in-scope on the right side. this is exactly like defining prolog relations. unlike prolog, though, this is the one structure, whereas prolog has relations and lists.

example of vseqs: a tree/graph that supports arbitrary traversals: simply create a map whose keys permit multiple orders. rather than "designing data structure", design your indexes. any data structure is definable by 2 fns: i→j, pred→i.

Note
when using virtual (instead of literal) code, homoiconicity is irrelevant. indeed, it actually sucks; we don’t want to bother with implementation details, with what the code is literally doing, nor with what internal representations / model the code system uses. we just care what the primitives are. furthermore, the primitives should be devised s.t. we don’t have to worry about how to use them efficiently.
Note
any structure is plurality-agnostic. operations parameterized over a set or seq works implicitly for an atom by making that atom a singleton set or seq.

a predicate is a constrained virtual relation e.g. x+5,x>6. , ("and") & ; ("or") are predicate primitives. predicates specify vseqs.

+

"together": for sets: union; for seqs: append

-

"apart": for sets or seqs: without values or indices

×

"each" (1:1 map)

÷

"distinct": for predicate and set/seq, group into subsets subseqs; given two structures, return the structures without each other

subset or subseq/find ("find atom" is equivalent to subseq for its singleton)

  • - (not) is the same as filter, without, and probably other common operations. because filter is actually just group, we see that all these are just division i.e. distinction i.e. difference: the extent or fact of things being distinct.

  • confuse, distinguish, assoc-merge is actually just coincidence (intersection) plus confusion: select f(v1,v2) from t1 join t2 using (k).

  • find(x,y) is just a relation of x & y. it’s a particular version of filter (i.e. intersect (w/predicate)!): first(filter(x,p) order by i)

from my phone notes:

+: together, harmony -: disharmony, disagreement ×: each. specific variety of addition. also alist. div: distinction, group

* sql relations sensibly generalize maps from a vector of (k,v) to a vector of arbitrary-length vectors. better yet, forget the constraint that all element vectors have equal length, or even that the elements be vectors at all! just map over vectors of whatever the hell! this is about where iversonian languages and sql fail; neither supports a good syntax for specifying predicates[prolog]/relations of arbitrary subsets of structures. part of the reason that sql does not is efficiency; sql exploits indexes [sql] greatly.

  • i used find instead of in? because specifying the membership predicate at call site is sensible, whereas defining equality for a type is needlessly inflexible.

    • always be skeptical of a function that doesn’t take a predicate. this is not a problem in j where we, instead of designing functions to take predicate arguments, compute masks then apply them wherever they may be used (possibly in multiple places or after being modified).

  • sets & seqs can be both defined in terms of maps; a map’s keys are a set, and a map’s values are a sequence if we order by keys.

  • if we use an array model, then push & append are equivalent, as are , and deletion & set difference, and same with union & adjoin, but for maps, though, assoc-merge : Map a b → Map a c → (b → c → d) → Map a d is a very useful operation. it generalizes zipWith/2map.

    • be skeptical of any function of collections. all functions which specifically take collections as args should use those collections in total, not only considering one element at a time! use a damn loop, then! for example, subseq? is a good function since it actually considers its inputs' elements' (ordinal) relations to each other.

      • if a function that could be used for a set is used on a sequence, then that function probably shouldn’t exist unless it’s a primitive. for example, map applies equally to a set as to a sequence, since it does not concern any relations that the collection’s elements may have with each other (in a seq, they’re related to each other by index), so map should not be defined, except that it’s a special case because it’s a primitive.

  • many redundant functions exist because they’re more efficient, such as map-filter. this is the language defect of making operations literal instead of virtual. for a programmer to care about, or write, or use/reference/familiarize themselves with such fluff is to have the programmer not only distract and burden themselves, but to actually make them think that it’s worthwhile, too, for the efficiency gain. it’s one of the cardinal sins of coding.

    • a related cardinal sin is convenience functions, which distract from the programmer’s holy connection to true primitives. an example is factor’s : gather ( …​ ) map concat members ; inline. if it’s done to make code briefer, then it’s at the cost of bloating the function namespace! the cost nullifies the benefit. besides, the actual solution is to make the syntax briefer. the same thing in k is ?,/.

  • all tests e.g. subseq? are better as parsers e.g. subseq which returns a subsequence matching a parser or a failure value.

  • nub exists for seqs, but it’s probably not the operation that you want; you probably want to convert the seq to a set.

  • seqs are generally multisets, but never multimaps, but we can store a structure as a value in a sequence, so who cares?

  • predicates [higher-order fns] suck. masks are better. masks tend to suck in apls because apls aren’t concatenative, so the common "generate mask by applying predicate to vector, then modify the resultant mask, then filter vector by modded predicate" pattern is clumsy or ugly. in a stack lang this literally would be e.g. OBJ dup [ PRED ] map 1 6 [ set-nth ] keep filter-by-mask. normally in factor you’d just say OBJ [ PRED ] filter but you can’t say that with "but keep the 6th elt." generally this is the problem with functional programming: it makes symmetry neat, but asymmetry clumsy.

    • TODO: id some examples of masks bettering predicates?

  • an intersection of predicates is equivalent to iterated filters. perform them in order of complexity e.g. if one predicate is testing for divisibility by 5, and another tests a relation of a window fn, then perform the 5| first, since it’ll be a subset, and we want to perform no more window fns than we must.

notice that i put "sequence" instead of "stack", "list", "deque" etc. sequences are generally virtual. i can have a sequence defined as "[1..6] rotated by 2 but where the last element is 12" which, being virtual, would be defined as a function in most langs: : my-funny-seq ( i — e ) 1 6 [a..b] 2 <rotated> over over length = [ 2drop 12 ] [ nth ] if ;. in the ideal lang it would be defined: 1 6 [a..b] 2 [ `last 12 ] add-idx. this is direct; it generates a virtual sequence from the constructor [a..b] then modifies its indexing fn by adding rotation, then we add the asymmetric rule that the value at index `last (a symbol literal) is 12. virtual sequences are an example of how k’s function/indexing duality is appropriate, though the irony is that k doesn’t use virtual sequences, so the duality’s benefit is only terser notation. but anyway, functions are virtual maps from the set of valid inputs to their corresponding outputs, so indexing into them is mathematically equivalent to calling them! to see a language that actually acknowledges not duality, but equivalence, and thus has virtual relations as its only data structure would be literally perfect, and would definitely benefit from that one structure being optimized, as is the case in prolog, sql, and apls. prolog might technically satisfy this condition, but its ergonomics don’t make it obvious. virtual seqs/rels unify all structures, but a notable subset of them is generators [python], loops, and (non-strictly evaluated) lists, and makes fusion automatic e.g. "," join print truly composes join & print, rather than "composing them", meaning to simply sequence their execution. atomic [j,k] operations can be fused since they necessarily have the same traversals.

what composites do we commonly want, or what composites would easily enable us to code arbitrary relations?

how to design programs (to be made into a poster! :D)

first, the fundamentals of code:

  1. the rawest programming is neural networks refined by a train of trials against an objective function. this variety is not logical.

  2. the rawest abstract (symbolic) logical programming is specifying relations, as we see in prolog: just specify predicates. each predicate corresponds to a set. predicate sets can answer queries.

  3. the rawest reductive (data-based) logical programming is manipulating byte sequences.

  4. the next-rawest abstract programming is functions, which are just unidirectional relations, and thus, unlike relations, have deterministic execution order (except parallel operations). functions are thus apt for describing a common variety of program called dataflows. functions, given inputs, can be evaluated for effect. you might ask, "what’s the use of just transmuting and moving around data?" indeed there is no use in it! what we actually ultimately do here is to create a sequence of associations between data subsets and hardware—what are called effects, such as sending data to a socket, or video device, or output stream. all the functions do is confound or distinguish subsets. confoundment may or may not be reversable; e.g. + is not, but (,) is. to keep memory use low, some subsets are marked for deallocation. because functional programming concerns sets & sequences, functional primitives are set-&-sequence operations (ins/ovr [at indies], get [at indices]). indices are specified by a predicate. a common variety of "get" is "get by predicate and its complement" i.e. "partition". the predicate returns distinct values which distinguish the subsets.

  5. all other programming is one of the prior plus some assumed, unnecessary model(s), which may be convenient for reasoning or describing things, but are necessarily limiting.

all coding is merely identifying/relating arbitrary subsets from the program’s total dataverse. and again: relations may be ordered (a/symmetric) or not, but must always be coded in an order. in the string "abc", a is related to b, and b to c, and so transitively a to c. any data structures that represent sets (e.g. tree set, hash set, bloom filter) are just sequences and algebra. structures generally are equivalent to virtual seqs, but are coded to exploit some algebraic properties, to make indexing & modification efficient. vseqs may be coded to be especially efficient for particular operations. this is necessarily accomplished by exploiting algebraic properties. in other words, all data are vseqs, yet the only reason that "other data structures" exist is that they are more constrained in what they can do or store, and those constraints reduce the structure’s entropy (so to speak; really variety), thus enabling specialized, simpler traversals.

b/c seqs have numerical indices, which are totally ordered, slices (substrs) are common. efficient traversals, such as binary search, are easy for this reason, too. generally, though, selection is done by filter: applying a predicate to (k,v) pairs. again, we’d ideally use filter with wonton abandon, but that’s inefficient, so we try to exploit what constraints we know for efficient traversal. ideally we’d apply these constraints to each structure object itself; rather than instantiating from a class [template], we’d create a copy of a prototypical structure (such as was done in js 5 before classes were introduced in v6) then imbue it with constraints, which would be coded in such a way that they naturally change the indexing of this vseq. this is natural inheritance [oop] given by objects' predicates' subsets (object A is a subclass of B to the extent that its predicates are a subset of B), rather than expressed explicitly by special class semantics and an extends keyword.

Note
conditional branching is expressed by maps whose cod is programs. if that map’s dom is bool, then cond; if literal values, then case. NOTE: number’s algebras can be exploited very powerfully & elegantly. see ./coding.adoc.

each language has a model for representing & evaluating programs. popular ones:

language model

assembly

traverse sequence in order declared

applicative

traverse ast depth-first

stack

modify stack until words run-out

logic

unify predicates

i should be able to unify associative & logical reasoning by: all are maps i.e. indexed sets i.e. associations and the logical primitive, predicate, is isomorphic with set. TODO: consider this later.

mutation vs purity

deletion (mutative) may be thought of as "without" (pure). thinking purely helps you realize that you’re just identifying thoughts rather than performing actions. it also saves you from sequencing things. however, it usually leads to less-efficient code, and is sometimes unnatural. note that it is necessary for parallel programming. if you’re sequencing some actions, then code it as such; if you’re specifying a constraint, then code it as such; if you’re specifying a composite relation, then code it as such! choose a good system that enables all these three: mutative, predicate, pure. if you don’t have such a system, then at least code the actual code in a comment near the implementation.