skip to content

Babble, [2025-09-17 Wed 23:05], Drafting a Fennel library for semiotics

I want to look at encoding Moscow school of semiotics in Fennel. This is a continuation of Babble, [2025-09-16 Tue 18:25]​

So, there are a few different structures in this school of semiotics, and they relate through various processes that rely on the structures having various conditions of various values.

This is a trickily specific way to describe it though, so let me go slow.

The structures I see are semiospheres, codes, signs (icons, indexes, symbols), text.

In order for each of these structures to exist, they have to be induced by something.

The semiosphere is induced by there being more than one code that share signs, so it would need to know about codes, and specific codes, and signs, and specific signs.

(I say "know about," I think I mean "relate to" or "be able to translate from" or something.)

A code is induced by there being a sign, so it needs to know how to… interpret that sign? But a code is essentially just at least one sign and the rule for how to interpret that sign within the code.

Signs are induced by there being relationship between two things. Here's where I could see it getting tricky: can a sign only have relationships with other signs, or is there some "real" relationship between a sign and some non-sign thing. This is where I think two-dimensionalist conceptual analysis could be useful.

I might say that an index is a secondary intension, while icons and symbols are forms of primary intension that rely on different modes of interpretation (i.e. sensory-familarity or semiosphere-familiarity)?

Which is to say that index-signs would need to point outside the semiosphere to specific quantified/computable things, symbol-signs would need to point to other signs in their semiosphere, and icon-signs would need to point to a representation.

Thinking about this in MUD terms, lets look at a room:

(local room
       {:name "hovel"
        :description "This is a dingy little hovel."
        :exits {:out "forest"}})

So, we have some form of structure, {}, and in it we have three different signs, to start with:

I think it's useful to say that we are effectively saying "within the current scope, we are saying that the sign 'name' is a symbol pointing to what follows."

Looking at each of these in turn: name and description aren't really signs at all, but texts: a bounded configuration of signs within a code. Which means we need ways to process the text - semiosis.

Exits can be understood as an index, I think, with out being a symbol that points to a symbol.

Now, there's some fuzziness in what I just said. Pointing to, for example, is a very shoddy way to say "can be interpreted as," which is more useful because it brings back in that this is about processing and interpretting.

A lot of that is what the Fennel interpreter does, to be clear, so some of this is actually exploring what parts of Fennel match, functionally, with various parts of semiotics, and then filling in the gaps, I guess.

It should be noted I guess that the room itself, its structure, is what we might name an object, but as we see, it is actually just a collection of signs.

I also should note that I see name and description both being primary intensions, while exits is a secondary intension. If the description were autogenerated from the name, I think that would make it a secondary intension.

To understand name the interpreter can either point to sensory-familiarity - reader memory of "hovel" - or could look in the semiosphere - other mentions of hovel and how they can be interpreted in this code.

This is where I think some idea of "rules" comes in: the way of the "how" in "how they can be interpreted." I.e. the code for the semiotics of rooms in MUDs needs rules for how to understand and interpret "name" when it comes across it. (And these rules will probably be handcoded processes: if name is being interpreted by a user, return the assigned sign. If description is being interpreted, look for a "description" symbol and if it links to a text, return that text, otherwise, return the name and exits.)

At this point in my babbling I'm also curious how these ideas would be expressed in LISP, specifically in accordance with R7RS.

Let me… try a different thing.

(local world {:map {:texts {} :rules {}}
              :objects {:texts {} :rules {}}})

(set world.map.texts.hovel {:name "hovel"
                      :description "Dirty hovel"
                      :exits {:out "forest"}})
(set world.map.texts.forest {:name "forest"
                             :description "Clean forest"
                             :exits {:in "hovel"}})

(fn world.map.rules.describe [area]
  (print (.. "area: " area.description)))

(set world.objects.texts.ball {:name "ball"
                               :description "Oblong"
                               :mass 5})

(fn world.objects.rules.describe [object]
  (print (.. "object: " object.description)))

(fn find-rules [rule-id]
  (var results [])
  (each [code-name code (pairs world)]
    (let [rule (?. code :rules rule-id)]
      (when rule (table.insert results {:rule rule :code code-name}))))
  results)

(fn find-texts [text-id]
  (var results [])
  (each [code-name code (pairs world)]
    (let [text (?. code :texts text-id)]
      (when text (table.insert results {:text text :code code-name}))))
  results)

(fn interpret [rule text]
  (let [rules (find-rules rule)
        texts (find-texts text)]
    (if (and (next rules) (next texts))
        (do (var chosen [])
            (var closest-dist math.huge)
            (each [_ r (ipairs rules)]
              (each [_ t (ipairs texts)]
                (let [dist (if (= r.code t.code) 0 1)]
                  (if (< dist closest-dist)
                      (do (set closest-dist dist)
                          (set chosen [{:rule r.rule :text t.text :distance dist}]))
                      (= dist closest-dist)
                      (table.insert chosen {:rule r.rule :text t.text :distance dist})))))
            (each [_ pair (ipairs chosen)]
              ((. pair :rule) (. pair :text))))
        (print "no matches found for " rule " " text))))

(interpret :describe :hovel)
;; "area: A dirty hovel"
(interpret :describe :ball)
;; "object: Oblong"
(interpret :move "ball to hovel")
;; no matches found for move ball to hovel

Aha! That did something pretty close to what I wanted, albeit badly and clunkily.

There's a world - that's our semiosphere, and in that world there's a map and objects - those are two different "codes" of being in the semiosphere.

Each code has texts and rules, and then there's an interpret function that takes a rule and a text and uses some overly basic math to sort out which rules and texts match, and then, which of those have the closest distance between them. Here, that's just, do they exist in the same code or not?

Now, the code above is pretty clunky: hard to read and inelegant. Given that texts are always tables and rules are always procedures, I can probably structure everything differently. But, I don't actually recall much of how that works in Fennel.

We'll have a table, semiotics, that represents our library: functions for making semiospheres and other structures, and for letting them behave like the things they are.

(It's at this point I'm thinking about making something called something like "flume": Lume, but done in Fennel, since I can tell i'm going to want the same utility functions over and over again, but also, my own utility functions (i.e. quibbling strings) not just what Lume does.)

(local semiotics {})

(fn semiotics.operative? [semiosphere thing]
  (= (type thing) :function))

(fn semiotics.find-sign [semiosphere sign ?operative]
  (let [results {}
        add-result (fn [code-name link]
                     (table.insert results {:code code-name
                                            : link}))]
    (each [code-name code (pairs semiosphere.codes)]
      (let [link (?. code sign)
            add-link (fn [] (add-result code-name link))]
        (when link
          (if (= ?operative nil)
              (add-link)
              (if (and (= ?operative true)
                       (semiosphere:operative? link))
                  (add-link)
                  (and (= ?operative false)
                       (= (semiosphere:operative? link) false))
                  (add-link))))))
    results))

(fn semiotics.interpret [semiosphere operation text]
  (let [operations (semiosphere:find-sign operation true)
        texts (semiosphere:find-sign text false)]
    (if (and (next operations) (next texts))
        (do
          (let [matches []]
            (each [_ matched-operation (ipairs operations)]
              (each [_ matched-text (ipairs texts)]
                (table.insert matches {:operation matched-operation
                                       :text matched-text
                                       :distance (if (= matched-operation.code
                                                        matched-text.code)
                                                     0
                                                     1)})))
            (var min-distance nil)
            (each [_ matched (ipairs matches)]
              (if (or (= min-distance nil)
                      (< matched.distance min-distance))
                  (set min-distance matched.distance)))
            (let [closest-matches []]
              (each [_ matched (ipairs matches)]
                (when (= matched.distance min-distance)
                  (table.insert closest-matches matched)))
              (each [_ closest-match (ipairs closest-matches)]
                (closest-match.operation.link closest-match.text.link)))))
        (print "no matches found for" operation text))))



(fn semiotics.generate-semiosphere []
  {:codes {}
   :find-sign semiotics.find-sign
   :interpret semiotics.interpret
   :operative? semiotics.operative?})

(fn make-world []
  (let [world (semiotics.generate-semiosphere)
        codes world.codes]
  (set codes.map {:hovel {:name "hovel" :description "Dirty hovel" :exits {:out "forest"}}
                  :forest {:name "forest" :description "Clean forest" :exits {:in "hovel"}}})
  (fn codes.map.describe [area]
    (print "area: " area.description))
  (set codes.objects {:ball {:name :ball :description "Oblong"}})
  (fn codes.objects.describe [object]
    (print "object: " object.description))
  world))

(let [bing (make-world)]
  (bing:interpret :describe :hovel)
  (bing:interpret :describe :ball))

That felt like a pretty good improvement over the first run-through, but still missed a lot of what I think will be critical to properly capture what I'm going for. (Whether that's an accurate rendition of Moscow-school semiotics or not, I'm not sure.

The biggest thing is that interpret does too much heavy lifting, in a way that doesn't really make space for doing things better.

See, the function I wrote, I can tell, is influenced by having made MUD command parsers: there's the first word, a verb, and everything else, which gets passed ot the function that interprets that verb.

That's honestly fine for now - it almost lets me REPL the semiosphere.

But what I want is for each interpretative act to transform the semiosphere in some way (even if that's just affirming the interpretation). This means I'll want to keep a journal of every transformation that occurs.

I also… want to make space for like, autocommunication and translation, which I think means that each code needs to be able to interpret and translate its own and other texts, rather than that live at the level of the semiosphere.

I also have this picture that a text is also able to interpret itself against its own code, which I think would make another code that's very close to the text's code, and there we get into needing a lot more nuance around that stuff, so I think this is a good spot to stop for now.

Backlinks

Created: 2025-10-05 Sun 17:39