From 7e5d0fcc39fd8a683fc7010af064849b454b432b Mon Sep 17 00:00:00 2001 From: Marshall Lochbaum Date: Sat, 4 Jun 2022 17:40:31 -0400 Subject: Further editing --- docs/doc/lexical.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'docs/doc/lexical.html') diff --git a/docs/doc/lexical.html b/docs/doc/lexical.html index dcc5fef0..290a2a37 100644 --- a/docs/doc/lexical.html +++ b/docs/doc/lexical.html @@ -6,7 +6,7 @@

Lexical scoping

BQN uses lexical scope, like most modern functional programming languages including Javascript, Scheme, and Julia, and like Dyalog APL's dfns (tradfns are dynamically scoped). This document describes how lexical scoping works, and a few small details relevant to BQN's version of it.

-

In short, every block is a separate scope that can refer to identifiers in containing scopes. When evaluated, the block makes a variable for each identifier defined in it (including arguments and operands). The blocks that it contains will now access these variables. In the first level of a block, variables must be defined before they can be used, but in child blocks, a variable can be used regardless of where it's defined, as long as the definition is evaluated before the child block is.

+

In short, every block is a separate scope, but can use identifiers in containing scopes. Each time it's evaluated, the block makes a variable for each identifier defined in it (including arguments and operands). The blocks that it contains might access these variables. At the top level of a block, identifiers must be defined before they can be used, but in child blocks, an identifier can be used even if it's defined later, as long as that use isn't evaluated before the definition can set the variable value.

Scopes

Scoping is a mechanism that allows the same variable name to refer to different variables depending on program context. For example, the following code uses the name a in two ways: once for a value at the top level, and once locally in a function. With scoping, once you write {} to create a block, you can define any name you want inside without worrying whether it's taken.

↗️
    a  6
@@ -18,7 +18,7 @@
 6
 

Above, the scope of the first a is the entire program, while the scope of the second a is limited to the body of F. So one form of context is that a name might mean different things depending on which block contains it. But even the exact same instance of a name in the source code might mean multiple things! A second kind of context is which evaluation of a block uses the name.

-

Without this ability BQN would be pretty limited: for example, an object's fields are variables. If the variable value didn't depend on what object contained it, there could effectively only be one instance of each object! While it's needed all the time, the most direct way to demonstrate one name meaning multiple things is with recursion. The (fragile) function below labels each element in a nested list structure with its index in the list containing it.

+

Without this ability BQN would be pretty limited: for example, an object's fields are variables. If the variable value didn't depend on what object contained it, there could effectively only be one instance of each object! It's not the most common use case, but recursion is the most direct way to demonstrate a name meaning multiple things at once. The (fragile) function below labels each element in a nested list structure with its index in the list containing it.

↗️
    Label  { i↕≠𝕩  i  𝕊=¨ 𝕩 }
 
     Label "ab"8, 765
@@ -36,7 +36,7 @@
 

Each call creates the list of indices i, then calls itself using 𝕊 on each element of 𝕩 if it's a list, then couples i to the result. This requires i to be unaffected by other calls to the function, which works because i is scoped not only to the source code location but also to the particular evaluation of the block that creates it.

These examples probably work like you expect—they're meant to highlight the features that scoping should have, in order to help show how less intuitive cases work later on.

Visibility

-

A scope can view and modify (with ) variables in other scopes that contain it. We say these variables are visible in the inner scopes. Variables at the top level of a program are visible to all the code in that program, so that we might call them "global". That would be a little misleading though, because for example each file is an entire program, so if one file is imported from another then it can't read the first file's variables.

+

A scope can view and modify (with ) variables in other scopes that contain it. We say these variables are visible in the inner scopes. Variables at the top level of a program are visible to all the code in that program, so that we might call them "global". That's somewhat misleading, because for example each file is an entire program, so if one file is imported from another then it can't read the first file's variables.

↗️
    counter  0
     inc  6
     Count  { counter + 𝕩 × inc }
@@ -108,7 +108,7 @@
 

Each result keeps its own counter and the different copies don't interfere with each other. This is because every call to _makeCount is isolated, happening in its own world. We say that whenever a block begins execution it creates an environment where all its variables are stored. This environment might even be exposed later on, as a namespace.

Each counter function has access to the environment containing its counter and inc, even though the block that created that environment (_makeCount) has finished execution—it must have finished, since we are now using the function it returns on the last line. There's nothing particularly weird about this; just because a block creates an environment when it starts doesn't mean it has to destroy it when it finishes. From the mathematical perspective, it's easiest to say the environment exists forever, but a practical implementation will perform garbage collection to free environments that are no longer reachable.

-

Since a function like C1_4 maintains access to all the variables it needs to run, we say it encloses those variables, and call it a closure. It doesn't need to modify them. For example, even the following definition of stdDev is a closure.

+

Since a function like C1_4 maintains access to all the variables it needs to run, we say it encloses those variables, and call it a closure. It doesn't need to modify them. For example, the following definition of the theoretical standard deviation function StdDev is also a closure.

stdDev  {
   # Arithmetic mean
   Mean  +´ ÷ 
@@ -127,7 +127,7 @@
 

We've seen that one block can create many environments. An environment can have only one parent, but many children, so environments form a tree. A forest to be precise, as one execution of BQN can involve multiple programs.

How does an environment know which of the many environments corresponding to the parent scope is its parent? This information is saved when the block is reached in the program and a block instance is created. Unless it's an immediate block, the block instance won't be run right away: a block instance isn't the same as a block evaluation. But each block evaluation starts with a block instance, and that's where it gets the parent environment. Unlike block evaluation, which can happen anywhere, a block instance is created only during evaluation of the parent block. So the saved parent environment is simply the current environment.

Mutation

-

The value of a variable can be modified with . It's similar to definition in that it sets the value of the variable, but the way it interacts with scoping is completely different. Defining creates a new variable in the current scope, and modifying refers to an existing variable in the current scope or a parent. In scoping terms, modifying is more like an ordinary variable reference than a definition.

+

The value of a variable can be modified with . It's similar to definition in that it sets the value of the variable, but the way it interacts with scoping is completely different. Definition creates a new variable in the current scope, and modification refers to an existing variable in the current scope or a parent. In scoping terms, a modification is more like an ordinary variable reference than a definition.

When a variable's modified, functions with access to it see the new value. They have access to the variable, not any particular value that it has.

↗️
    factor  3
     Mul  { factor × 𝕩 }
@@ -138,7 +138,7 @@
     Mul 6   # A new result
 30
 
-

Only code with access to a variable can modify it! This means that if none of the code in a variable's scope modifies it, then the variable is a constant in each environment that contains it (not necessarily across environments). That is, constant once it's defined: it's still possible to get an error if the variable is accessed before being defined.

+

Only code with access to a variable can modify it! This means that if none of the code in a variable's scope modifies it, then the variable is a constant in each environment that contains it (not necessarily across environments). That is, constant once it's defined: remember that it's still possible to get an error if the variable is accessed before being defined.

↗️
    { { a }  a4 }
 Error: Reading variable before its defined
 
-- cgit v1.2.3