Reactive Script

Basically all you need is language level read/write segregation, and the ability to define getters and setters for all values?

^value means: The permission to read value but not value itself. It's basically the getter

value^ means: the permission to write to value but not value itself. it's basically the setter

console.log(value): when value is used alone without the carrots, the getter is called, then the actual data itself is passed to the function

let value = "data" when let and = is used. that is showing the inital setting of the value.

value = "data" in this, = without let is the reassignment operator, this calls the setter.

let value^ = "data" this shows how to define a variable that is writable, but not readable.

let ^value = "data" this shows how to define a variable that is readable, but not writable

let ^value^ = "data" this shows how to define a variable that is readable and writable. Maybe this is the default?

let ^getter value setter^ = "data" this shows show to explicitly define getters & setters this can also be an expression or funciton that returns a function

\macroName value = "data" this shows how to use a macro, which will take in the value, it's type, and a string of the source, and generate the getters / setters themselves. Easy getter/setter setting.

Hello(^name) this means you are passing "The permission to read name" not the value of name itself.

Hello(name^) this means you are passing "The permission to write to name" not the value of name itself.

Hello(name) basically unwraps it, and passes the value, without getters or setters to the function. It basically loses it's identity.

function Hello (^name: String) {} this means you are accepting "The permission to read name" not the value itself.

function Hello (name^: String) {} this means you are accepting "The permission to write to name" not the value itself. When set inside the function. The parameter isnt just changing. The canonical name variable is also changing. It's like the parmeter takes in / shadows the identity of the passed value.

You define what fields are readable / writable up front

type Person = {
	^name^: String, // This is readable and writeable.
	^age  : Number, // This is readable but not writable.	
	setPin^ pin: Number, //This can not be read, only set.
}

// When actually creating the object, you can assign the getters/setters. 
let Patrick: Person = {
	^getName name setName^ : "Patrick", 
} 

let nickName = (^shortName: String) => {
	Patrick.name = shortName
}

function getName (newValue, currentValue, personObject) {
	console.log(newValue)
	return currentValue
}

Identity

In programming, identity goes beyond the current value of a variable. It represents the essence of a mutable entity that persists even as its value changes over time. Think of identity like a name tag: the name stays the same, even if the person wearing it changes their clothes or hairstyle.

In Reactive Script, the caret symbol (^) is all about preserving and passing this identity.

How ^ Maintains Identity

When you use ^ in Reactive Script, you're not just dealing with the current value of a variable, but with its persistent identity. This means:

  1. You're working with the actual variable, not a copy of its value.
  2. Changes to this identity are reflected everywhere that identity is used.
  3. You maintain a consistent reference to the variable, regardless of how it's passed around.

Let's see this in action:

let ^counter^ = 0

function increment(^num^) {
    num = num + 1
}

increment(^counter^)
console.log(counter)  // Outputs: 1

Here, ^counter^ establishes an identity for counter. When we pass ^counter^ to increment, we're passing this identity, not just the value 0. The function then works directly with the counter variable, not a copy of its value.

Passing Identity

The caret allows you to explicitly pass identity between functions and objects. This is powerful because it allows functions to work directly with the original data, rather than copies or indirect references.

type User = {
    ^name^: string,
    ^age^: number
}

let user = {
    name: "Alice",
    age: 30
}

function haveBirthday(^person: User) {
    person.age = person.age + 1
    person.name = person.name + " 🎉"
}

haveBirthday(user)
console.log(user)  // Outputs: { name: "Alice 🎉", age: 31 }

In this example, haveBirthday receives the identity of user, not just its current values. This allows the function to modify the original user object directly.

Identity vs Value

To understand the power of identity, let's compare it with traditional value passing:

// Traditional value passing
function incrementValue(num: number): number {
    return num + 1
}

let count = 0
count = incrementValue(count)
console.log(count)  // Outputs: 1

// Reactive Script identity passing
function incrementIdentity(^num^: number) {
    num = num + 1
}

let ^count^ = 0
incrementIdentity(^count^)
console.log(count)  // Outputs: 1

While both achieve the same result, the identity-based approach allows for direct modification, which can be more intuitive and efficient, especially in more complex scenarios.

Partial Identity: Read-Only and Write-Only

Reactive Script allows for fine-grained control over identity with read-only (^value) and write-only (value^) syntax:

type Sensor = {
    ^id: string,
    ^reading^: number
}

function updateReading(sensor: Sensor, newReading: number) {
    sensor.reading = newReading  // This works
    // sensor.id = "new id"  // This would cause an error
}

let tempSensor: Sensor = {
    id: "temp001",
    reading: 22.5
}

updateReading(tempSensor, 23.1)
console.log(tempSensor.reading)  // Outputs: 23.1
console.log(tempSensor.id)       // Outputs: "temp001"

Here, ^id allows reading the id but not changing it, while ^reading^ allows updating the reading and directly accessing its value.

Identity in Nested Structures

The concept of identity extends to nested structures, allowing for precise control over complex objects:

type Department = {
    ^name: string,
    ^employees^: {
        ^id: number,
        ^name^: string,
        salary^: number
    }[]
}

let engineering: Department = {
    name: "Engineering",
    employees: [
        { id: 1, name: "Bob", salary: 75000 },
        { id: 2, name: "Alice", salary: 82000 }
    ]
}

function giveRaise(^dept: Department, employeeId: number, amount: number) {
    const employee = dept.employees.find(e => e.id === employeeId)
    if (employee) {
        employee.salary = employee.salary + amount
    }
}

giveRaise(^engineering, 1, 5000)
console.log(engineering.employees[0].salary)  // Outputs: 80000

In this example, we can modify the salary of an employee deep within the engineering department structure, all while maintaining the identity and access controls of each level.

Conclusion

The caret symbol (^) in Reactive Script is a powerful tool for maintaining and passing identity. It allows for more intuitive handling of mutable state, fine-grained control over data access, and clear communication of a programmer's intentions regarding data flow and modification. By making identity explicit, Reactive Script encourages a programming style that can lead to more predictable and maintainable code, especially in complex, data-intensive applications.

Store Example

Carets (^) are only used where maintaining the identity of the value is important:

^value and value^ in the Store type to allow direct reading and writing to the store.
^value^ inside the createStore function to maintain the identity of the internal value.

Regular value passing is used where identity isn't crucial:

subscribe functions now take and pass regular values.
createDerivedStore and createComputed functions use regular value passing.

Added examples to demonstrate the difference:

incrementCounter function takes ^counter: Store<number> to directly modify the store.
doubleCounter function takes a regular counter: Store<number> as it doesn't need to modify the store's identity.

// Define a type for our store
type Store<T> = {
    ^value: T,
    value^: (newValue: T) => void,
    subscribe: (listener: (value: T) => void) => () => void
}

// Create a function to create a new store
function createStore<T>(initialValue: T): Store<T> {
    let ^value^ = initialValue
    let listeners: ((value: T) => void)[] = []

    // Create the store object
    let store: Store<T> = {
        ^value: value,
        value^: (newValue) => {
            value = newValue
            listeners.forEach(listener => listener(value))
        },
        subscribe: (listener) => {
            listeners.push(listener)
            return () => {
                listeners = listeners.filter(l => l !== listener)
            }
        }
    }

    return store
}

// Create a derived store
function createDerivedStore<T, U>(sourceStore: Store<T>, deriveFn: (value: T) => U): Store<U> {
    let derivedStore = createStore(deriveFn(sourceStore.value))
    
    sourceStore.subscribe((value) => {
        derivedStore.value = deriveFn(value)
    })

    return derivedStore
}

// Example usage
let counterStore = createStore(0)

// Create a derived store
let doubleCountStore = createDerivedStore(counterStore, (count) => count * 2)

// Subscribe to changes
let unsubscribe = counterStore.subscribe((value) => {
    console.log("Counter value changed:", value)
})

let unsubscribeDouble = doubleCountStore.subscribe((value) => {
    console.log("Double count value changed:", value)
})

// Modify the store
counterStore.value = 1  // Logs: Counter value changed: 1, Double count value changed: 2
counterStore.value = 2  // Logs: Counter value changed: 2, Double count value changed: 4

// Unsubscribe
unsubscribe()
unsubscribeDouble()

// Create a complex store
type User = {
    name: string,
    age: number
}

let userStore = createStore<User>({ name: "John", age: 30 })

// Subscribe to user store
userStore.subscribe((user) => {
    console.log("User updated:", user.name, user.age)
})

// Update user
userStore.value = { ...userStore.value, name: "Jane" }  // Logs: User updated: Jane 30

// Create a computed value
function createComputed<T>(compute: () => T): Store<T> {
    let computedStore = createStore(compute())
    
    // This is where we'd set up dependencies and recompute as needed
    // For simplicity, we're not implementing a full dependency tracking system here

    return {
        ^value: computedStore.value,
        value^: () => { throw new Error("Cannot set a computed store directly") },
        subscribe: computedStore.subscribe
    }
}

// Example of a computed store
let ageNextYearStore = createComputed(() => userStore.value.age + 1)

ageNextYearStore.subscribe((age) => {
    console.log("Age next year:", age)
})

// Update user age
userStore.value = { ...userStore.value, age: 31 }  // Logs: User updated: Jane 31, Age next year: 32

// Attempt to set computed store (will throw an error)
// ageNextYearStore.value = 35  // Throws: Cannot set a computed store directly

// Example of a function that modifies the store directly
function incrementCounter(^counter: Store<number>) {
    counter.value = counter.value + 1
}

incrementCounter(^counterStore)  // This will increment the counter and trigger listeners
console.log("Counter after increment:", counterStore.value)

// Example of a function that doesn't modify the store's identity
function doubleCounter(counter: Store<number>): number {
    return counter.value * 2
}

console.log("Double of counter:", doubleCounter(counterStore))

Memory

Rust's lifetime system, while powerful, can sometimes lead to complex annotations, especially when dealing with structs that contain references. Reactive Script's identity and ownership system provides a different approach that can simplify many of these scenarios. Let's explore some examples:

1. Structs with References

In Rust, a struct containing a reference requires explicit lifetime annotations:

struct BookExcerpt<'a> {
    content: &'a str,
}

In Reactive Script, we can use the identity system to achieve similar safety without explicit lifetime annotations:

type BookExcerpt = {
    ^content: string,
}

function createExcerpt(^book: string): BookExcerpt {
    return {
        ^content: book.substring(0, 100)  // First 100 characters
    }
}

let ^fullBook^ = "Long book content..."
let excerpt = createExcerpt(^fullBook)

console.log(excerpt.content)  // Safe to use

Here, the ^content in BookExcerpt indicates that it's borrowing the content, but we don't need to specify for how long explicitly.

2. Self-Referential Structs

Self-referential structs are notoriously difficult in Rust. For example:

struct SelfRef {
    value: String,
    pointer_to_value: *const String, // Raw pointers used as a workaround
}

In Reactive Script, we can express this more safely and easily:

type SelfRef = {
    ^value^: string,
    ^pointerToValue: string,
}

function createSelfRef(initialValue: string): SelfRef {
    let ^value^ = initialValue
    return {
        ^value^,
        ^pointerToValue: value
    }
}

let ^selfRef^ = createSelfRef("Hello")
console.log(selfRef.value === selfRef.pointerToValue)  // true

The identity system ensures that pointerToValue is always valid as long as the struct exists.

3. Structs with Multiple Lifetime Parameters

Rust sometimes requires multiple lifetime parameters, which can be complex:

struct DoubleRef<'a, 'b> {
    x: &'a i32,
    y: &'b i32,
}

Reactive Script can handle this more intuitively:

type DoubleRef = {
    ^x: number,
    ^y: number,
}

function createDoubleRef(^x: number, ^y: number): DoubleRef {
    return { ^x, ^y }
}

let ^a^ = 5
let ^b^ = 10
let doubleRef = createDoubleRef(^a, ^b)

console.log(doubleRef.x, doubleRef.y)  // 5 10

The ownership and borrowing are clear from the function signature and struct definition, without need for explicit lifetime annotations.

4. Structs with Callbacks

In Rust, storing callbacks in structs can be challenging due to lifetime issues:

struct WithCallback<F>
where
    F: Fn(i32) -> i32,
{
    callback: F,
}

Reactive Script can handle this more flexibly:

type WithCallback = {
    ^callback: (x: number) => number,
}

function createWithCallback(^cb: (x: number) => number): WithCallback {
    return { ^callback: cb }
}

let ^double^ = (x: number) => x * 2
let withCallback = createWithCallback(^double)

console.log(withCallback.callback(5))  // 10

The identity system ensures that the callback remains valid, and the ownership is clear from the caret notation.

5. Mixing Owned and Borrowed Data

Rust often requires careful management when mixing owned and borrowed data:

struct MixedOwnership<'a> {
    owned_string: String,
    borrowed_string: &'a str,
}

Reactive Script can express this more straightforwardly:

type MixedOwnership = {
    ^ownedString^: string,
    ^borrowedString: string,
}

function createMixed(^owned: string, ^borrowed: string): MixedOwnership {
    return {
        ^ownedString^: owned,
        ^borrowedString: borrowed
    }
}

let ^myString^ = "Hello"
let ^mixed^ = createMixed("World", ^myString)

console.log(mixed.ownedString, mixed.borrowedString)  // World Hello

The ownership and borrowing relationships are clear from the caret notations, without need for separate lifetime annotations.

Conclusion

Reactive Script's identity and ownership system provides a powerful alternative to Rust's lifetime annotations, especially when dealing with complex struct scenarios. By using the caret notation to indicate ownership and borrowing relationships, Reactive Script can express many patterns that require explicit lifetime management in Rust, often with simpler and more intuitive syntax.

This approach allows for fine-grained control over ownership and borrowing without the need for a separate lifetime system, potentially simplifying code while still maintaining strong safety guarantees. It demonstrates how Reactive Script's unique features can address some of the challenges faced in systems programming languages like Rust.

Introduction

1. Core Concepts

1.1 Identity and Permissions

Reactive Script's fundamental feature is the caret (^) notation, which denotes identity and permissions:

  • ^value: Read permission and identity preservation
  • value^: Write permission and identity preservation
  • ^value^: Both read and write permissions with identity preservation

1.2 Ownership Tracking through Identity

The identity system in Reactive Script is primarily used for tracking ownership. When you use the caret notation, you're working with the identity of a variable, which allows the language to track ownership and manage resources more efficiently.

function transferOwnership(^resource: Resource) -> ^Resource {
    // This function can transfer ownership of the resource
    // without copying, using the identity system
}

1.3 Default Value Semantics

Without carets, Reactive Script uses default value semantics:

  • Primitives are passed by value
  • Objects are passed by reference, but without identity preservation

2. Macros

Macros in Reactive Script are general-purpose tools for code generation. They can be used to create any kind of code structure, including but not limited to getters and setters.

2.1 Macro Definition

\macro customMacro(parameters) {
    // Macro body: generates code based on parameters and usage context
}

2.2 Macro Usage

\customMacro myVariable = initialValue

The macro expands based on its definition, potentially generating getters, setters, or any other code structure.

2.3 Examples of Possible Macros

While signal, memo, and effect are not built-in features of Reactive Script, they could be implemented as macros:

\macro signal(initialValue) {
    // Could generate code for a mutable value with change tracking
}

\macro memo(computation) {
    // Could generate code for a computed value
}

\macro effect(sideEffect) {
    // Could generate code for a side effect
}

These are just examples. The actual implementation and behavior of these macros would depend on how they are defined by the programmer.

3. Power of the Identity System

The identity and permission system in Reactive Script is particularly powerful for tracking ownership and managing resources:

function borrowResource(^resource: Resource) {
    // This function can use the resource but can't transfer ownership
}

function moveResource(resource^: Resource) -> ^Resource {
    // This function can take ownership of the resource and transfer it
}

function useAndUpdateResource(^resource^: Resource) {
    // This function can both read from and write to the resource
}

This system allows for fine-grained control over how resources are accessed, modified, and transferred throughout the program, enabling efficient and safe resource management.

4. Custom Patterns with Macros and Identity

Programmers can combine macros and the identity system to create custom patterns:

\macro observableResource(initialValue) {
    // This macro could generate code that uses the identity system
    // to create an observable resource with ownership tracking
}

\observableResource myResource = initialValue

function updateResource(^myResource^: Resource) {
    // This function can modify the resource and potentially notify observers
}

Conclusion

Reactive Script's power comes from two main features:

  1. A flexible macro system that allows for arbitrary code generation and the creation of custom patterns.
  2. An identity and permission system that enables fine-grained ownership tracking and resource management.

The language itself does not provide built-in reactive features or specific macros like signal, memo, or effect. Instead, it offers powerful tools (macros and identity preservation) that programmers can use to implement various patterns, including reactive systems if desired. The caret notation provides precise control over data access, modification, and ownership, making Reactive Script suitable for building efficient and flexible systems with custom behavior.