Category talk:Wren-dynamic: Difference between revisions

From Rosetta Code
Content added Content deleted
(Enums can now start from any integral value, not just 0.)
m (Now uses Wren S/H lexer.)
 
(8 intermediate revisions by the same user not shown)
Line 1: Line 1:
===Generation of classes at runtime===
===Generation of classes at runtime===


There is often a need in Wren programming for simple classes which would be represented in some other languages by enums, 'flags' enums, tuples, structs or unions. None of these are supported directly by Wren and, although they can be easily simulated, they are rather tedious to write.
There is often a need in Wren programming for simple classes which would be represented in some other languages by enums, 'flags' enums, constant groups, tuples, structs or unions. None of these are supported directly by Wren and, although they can be easily simulated, they are rather tedious to write.


This module aims to rectify this by providing templates to generate such classes at runtime for simple cases. In particular:
This module aims to rectify this by providing templates to generate such classes at runtime for simple cases. In particular:
Line 7: Line 7:
* Enum values always start from an initial integer value (often 0) and are incremented by 1 which is the commonest case.
* Enum values always start from an initial integer value (often 0) and are incremented by 1 which is the commonest case.
* 'Flags' enums are always powers of 2, starting from 1 (= 2^0).
* 'Flags' enums are always powers of 2, starting from 1 (= 2^0).
* Groups represent other groupings of related named constants of any type.
* Structs and tuples both represent data classes but fields for the former are read/write and for the latter read only.
* Structs and tuples both represent data classes but fields for the former are read/write and for the latter read only.
* Union represents a value which can be any one of a set of types. A single storage location stores the value or a reference thereto.
* Union represents a value which can be any one of a set of types. A single storage location stores the value or a reference thereto.
Line 13: Line 14:
To create for example a Point tuple, one could proceed as follows:
To create for example a Point tuple, one could proceed as follows:


<lang ecmascript>import "/dynamic" for Tuple
<syntaxhighlight lang="wren">import "/dynamic" for Tuple


var Point = Tuple.create("Point", ["x", "y"])
var Point = Tuple.create("Point", ["x", "y"])
var p = Point.new(1, 2)
var p = Point.new(1, 2)
System.print([p.x, p.y, p]) // [1, 2, (1, 2)]</lang>
System.print([p.x, p.y, p]) // [1, 2, (1, 2)]</syntaxhighlight>


More complicated cases than these are best dealt with by manual programming as at present.
More complicated cases than these are best dealt with by manual programming as at present.


===Source code===
===Source code===
<lang ecmascript>/* Module "dynamic.wren" */
<syntaxhighlight lang="wren">/* Module "dynamic.wren" */


import "meta" for Meta
import "meta" for Meta
Line 29: Line 30:
Members are assigned in order an initial integer value (often 0), incremented by 1 each time.
Members are assigned in order an initial integer value (often 0), incremented by 1 each time.
The enum has:
The enum has:
1. static property getters for each member, and
1. static property getters for each member,
2. a static 'members' property which returns a list of its members as strings.
2. a static 'startsFrom' property,
3. a static 'members' property which returns a list of its members as strings,
4. a static '[member]' indexer which returns its integer value from its member name (string), and
5. a static 'fromValue(v)' method which returns the member name (string) from its integer value.
*/
*/
class Enum {
class Enum {
// Creates a class for the Enum (with an underscore after the name) and
// Creates a class for the Enum (with an underscore after the name) and
// returns a reference to it.
// returns a reference to it.
static create(name, members, start) {
static create(name, members, startsFrom) {
if (name.type != String || name == "") Fiber.abort("Name must be a non-empty string.")
if (name.type != String || name == "") Fiber.abort("Name must be a non-empty string.")
if (members.isEmpty) Fiber.abort("An enum must have at least one member.")
if (members.isEmpty) Fiber.abort("An enum must have at least one member.")
if (start.type != Num || !start.isInteger) Fiber.abort("Start must be an integer.")
if (startsFrom.type != Num || !startsFrom.isInteger) {
Fiber.abort("Must start from an integer.")
}
name = name + "_"
name = name + "_"
var s = "class %(name) {\n"
var s = "class %(name) {\n"
for (i in 0...members.count) {
for (i in 0...members.count) {
var m = members[i]
var m = members[i]
s = s + " static %(m) { %(i + start) }\n"
s = s + " static %(m) { %(i + startsFrom) }\n"
}
}
var mems = members.map { |m| "\"%(m)\"" }.join(", ")
var mems = members.map { |m| "\"%(m)\"" }.join(", ")
s = s + " static members { [%(mems)] }\n}\n"
s = s + " static startsFrom { %(startsFrom) }\n"
s = s + " static members { [%(mems)] }\n"
s = s + " static [member] {\n"
s = s + " var ix = members.indexOf(member)\n"
s = s + " return (ix >= 0) ? ix + %(startsFrom) : null\n"
s = s + " }\n"
s = s + " static fromValue(v) {\n"
s = s + " if (!v.isInteger || v < %(startsFrom) || v >= %(startsFrom) + members.count) {\n"
s = s + " return null\n"
s = s + " }\n"
s = s + " return members[v - %(startsFrom)]\n"
s = s + " }\n}\n"
s = s + "return %(name)"
s = s + "return %(name)"

return Meta.compile(s).call()
return Meta.compile(s).call()
}
}
Line 55: Line 73:
}
}


/* Flags creates a 'flags' enum with up to 32 read-only static members.
/* Flags creates a 'flags' enum with up to 32 read-only static members, plus an optional zero member.
Members are assigned in order an integer value starting from 1 and multiplying by 2 each time.
Members are assigned in order an integer value starting from 1 and multiplying by 2 each time, unless
they have a zero member, when the first member has a value of 0.
The flags enum has:
The flags enum has:
1. static property getters for each member, and
1. static property getters for each member,
2. a static 'members' property which returns a list of its members as strings.
2. a static 'hasZero' property which returns whether or not there is a zero member,
3. a static 'members' property which returns a list of its members as strings,
4. a static '[member]' indexer which returns its integer value from its member name (string), and
5. a static 'fromValue(v)' method which returns the member name (string) from its integer value.
*/
*/
class Flags {
class Flags {
// Creates a class for the Flags enum (with an underscore after the name) and
// Creates a class for the Flags enum (with an underscore after the name) and
// returns a reference to it.
// returns a reference to it.
static create(name, members) {
static create(name, members, hasZero) {
if (name.type != String || name == "") Fiber.abort("Name must be a non-empty string.")
if (name.type != String || name == "") Fiber.abort("Name must be a non-empty string.")
if (members.isEmpty ||members.count > 32) {
if (members.isEmpty || members.count > (hasZero ? 33 : 32)) {
Fiber.abort("A flags enum must have between 1 and 32 members.")
Fiber.abort("A flags enum must have between 1 and 32 members, plus an optional zero member.")
}
}
if (hasZero.type != Bool) Fiber.abort("'hasZero' must be true or false.")
name = name + "_"
name = name + "_"
var s = "class %(name) {\n"
var s = "class %(name) {\n"
for (i in 0...members.count) {
for (i in 0...members.count) {
var m = members[i]
var m = members[i]
s = s + " static %(m) { %(1 << i) }\n"
if (i == 0 && hasZero) {
s = s + " static %(m) { 0 }\n"
} else if (hasZero) {
s = s + " static %(m) { %(1 << (i-1)) }\n"
} else {
s = s + " static %(m) { %(1 << i) }\n"
}
}
}
var mems = members.map { |m| "\"%(m)\"" }.join(", ")
var mems = members.map { |m| "\"%(m)\"" }.join(", ")
s = s + " static members { [%(mems)] }\n}\n"
s = s + " static hasZero { %(hasZero) }\n"
s = s + " static members { [%(mems)] }\n"
s = s + " static [member] {\n"
s = s + " var ix = members.indexOf(member)\n"
if (hasZero) {
s = s + " if (ix == 0) return 0\n"
s = s + " return 1 << (ix-1)\n"
} else {
s = s + " return 1 << ix\n"
}
s = s + " }\n"
s = s + " static fromValue(v) {\n"
if (hasZero) {
s = s + " if (v == 0) return members[0]\n"
s = s + " if (!v.isInteger || v < 1 || v & (v-1) != 0) return null\n"
s = s + " var ix = (v.log / 2.log).round\n"
s = s + " if (ix + 1 >= members.count) return null\n"
s = s + " return members[ix+1]\n"
} else {
s = s + " if (!v.isInteger || v < 1 || v & (v-1) != 0) return null\n"
s = s + " var ix = (v.log / 2.log).round\n"
s = s + " if (ix >= members.count) return null\n"
s = s + " return members[ix]\n"
}
s = s + " }\n}\n"
s = s + "return %(name)"
s = s + "return %(name)"
return Meta.compile(s).call()
return Meta.compile(s).call()
}
}


// Returns the zero based index into the Fields property for a given Flags enum member.
// Convenience version of above method which does not have a member with a value of zero.
static indexOf(member) { (member.log / 2.log).round }
static create(name, members) { create(name, members, false) }
}

/* Group creates a group of related named constants of any type.
The group has:
1. static property getters for each member,
2. a static 'members' property which returns a list of its members as strings, and
3. a static 'values' property which returns a list of their corresponding values.
*/
class Group {
// Creates a class for the Group (with an underscore after the name) and
// returns a reference to it.
static create(name, members, values) {
if (name.type != String || name == "") Fiber.abort("Name must be a non-empty string.")
if (members.isEmpty) Fiber.abort("A group must have at least one member.")
if (members.count != values.count) Fiber.abort("There must be as many values as members.")
name = name + "_"
var s = "class %(name) {\n"
for (i in 0...members.count) {
var m = members[i]
var v = values[i]
if (v is String) {
s = s + " static %(m) { \"%(v)\" }\n"
} else {
s = s + " static %(m) { %(v) }\n"
}
}
var mems = members.map { |m| "\"%(m)\"" }.join(", ")
var vals = values.map { |v| (v is String) ? "\"%(v)\"" : "%(v)" }.join(", ")
s = s + " static members { [%(mems)] }\n"
s = s + " static values { [%(vals)] }\n"
s = s + "}\n"
s = s + "return %(name)"
return Meta.compile(s).call()
}
}
}


Line 184: Line 271:
}
}


/* The following enums generate the ASCII codes for digits, upper and lower case letters. */
// Type aliases for classes in case of any name clashes with other modules.

var Dynamic_Enum = Enum
var Dynamic_Flags = Flags
var Digits = Enum.create(
"Digits",
var Dynamic_Struct = Struct
["zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"],
var Dynamic_Tuple = Tuple
48
var Dynamic_Union = Union</lang>
)

var Upper = Enum.create(
"Upper",
"ABCDEFGHIJKLMNOPQRSTUVWXYZ".toList,
65
)

var Lower = Enum.create(
"Lower",
"abcdefghijklmnopqrstuvwxyz".toList,
97
)</syntaxhighlight>

Latest revision as of 12:11, 3 November 2023

Generation of classes at runtime

There is often a need in Wren programming for simple classes which would be represented in some other languages by enums, 'flags' enums, constant groups, tuples, structs or unions. None of these are supported directly by Wren and, although they can be easily simulated, they are rather tedious to write.

This module aims to rectify this by providing templates to generate such classes at runtime for simple cases. In particular:

  • Enum values always start from an initial integer value (often 0) and are incremented by 1 which is the commonest case.
  • 'Flags' enums are always powers of 2, starting from 1 (= 2^0).
  • Groups represent other groupings of related named constants of any type.
  • Structs and tuples both represent data classes but fields for the former are read/write and for the latter read only.
  • Union represents a value which can be any one of a set of types. A single storage location stores the value or a reference thereto.


To create for example a Point tuple, one could proceed as follows:

import "/dynamic" for Tuple

var Point = Tuple.create("Point", ["x", "y"])
var p = Point.new(1, 2)
System.print([p.x, p.y, p]) // [1, 2, (1, 2)]

More complicated cases than these are best dealt with by manual programming as at present.

Source code

/* Module "dynamic.wren" */

import "meta" for Meta

/* Enum creates an enum with any number of read-only static members.
   Members are assigned in order an initial integer value (often 0), incremented by 1 each time.
   The enum has:
   1. static property getters for each member,
   2. a static 'startsFrom' property,
   3. a static 'members' property which returns a list of its members as strings,
   4. a static '[member]' indexer which returns its integer value from its member name (string), and
   5. a static 'fromValue(v)' method which returns the member name (string) from its integer value.
*/
class Enum {
    // Creates a class for the Enum (with an underscore after the name) and
    // returns a reference to it.
    static create(name, members, startsFrom) {
        if (name.type != String || name == "") Fiber.abort("Name must be a non-empty string.")
        if (members.isEmpty) Fiber.abort("An enum must have at least one member.")
        if (startsFrom.type != Num || !startsFrom.isInteger) {
            Fiber.abort("Must start from an integer.")
        }
        name = name +  "_"
        var s = "class %(name) {\n"
        for (i in 0...members.count) {
            var m = members[i]
            s = s + "    static %(m) { %(i + startsFrom) }\n"
        }
        var mems = members.map { |m| "\"%(m)\"" }.join(", ")
        s = s + "    static startsFrom { %(startsFrom) }\n"
        s = s + "    static members { [%(mems)] }\n"
        s = s + "    static [member] {\n"
        s = s + "        var ix = members.indexOf(member)\n"
        s = s + "        return (ix >= 0) ? ix + %(startsFrom) : null\n"
        s = s + "    }\n"
        s = s + "    static fromValue(v) {\n"
        s = s + "        if (!v.isInteger || v < %(startsFrom) || v >= %(startsFrom) + members.count) {\n"
        s = s + "           return null\n"
        s = s + "        }\n"
        s = s + "        return members[v - %(startsFrom)]\n"
        s = s + "    }\n}\n"
        s = s + "return %(name)"

        return Meta.compile(s).call()
     }

     // Convenience version of above method which always starts from 0.
     static create(name, members) { create(name, members, 0) }
}

/* Flags creates a 'flags' enum with up to 32 read-only static members, plus an optional zero member.
   Members are assigned in order an integer value starting from 1 and multiplying by 2 each time, unless
   they have a zero member, when the first member has a value of 0.
   The flags enum has:
   1. static property getters for each member,
   2. a static 'hasZero' property which returns whether or not there is a zero member,
   3. a static 'members' property which returns a list of its members as strings,
   4. a static '[member]' indexer which returns its integer value from its member name (string), and
   5. a static 'fromValue(v)' method which returns the member name (string) from its integer value.
*/
class Flags {
    // Creates a class for the Flags enum (with an underscore after the name) and
    // returns a reference to it.
    static create(name, members, hasZero) {
        if (name.type != String || name == "") Fiber.abort("Name must be a non-empty string.")
        if (members.isEmpty || members.count > (hasZero ? 33 : 32)) {
            Fiber.abort("A flags enum must have between 1 and 32 members, plus an optional zero member.")
        }
        if (hasZero.type != Bool) Fiber.abort("'hasZero' must be true or false.")
        name = name + "_"
        var s = "class %(name) {\n"
        for (i in 0...members.count) {
            var m = members[i]
            if (i == 0 && hasZero) {
                s = s + "    static %(m) { 0 }\n"
            } else if (hasZero) {
                s = s + "    static %(m) { %(1 << (i-1)) }\n"
            } else {
                s = s + "    static %(m) { %(1 << i) }\n"
            }
        }
        var mems = members.map { |m| "\"%(m)\"" }.join(", ")
        s = s + "    static hasZero { %(hasZero) }\n"
        s = s + "    static members { [%(mems)] }\n"
        s = s + "    static [member] {\n"
        s = s + "        var ix = members.indexOf(member)\n"
        if (hasZero) {
            s = s + "        if (ix == 0) return 0\n"
            s = s + "        return 1 << (ix-1)\n"
        } else {
            s = s + "        return 1 << ix\n"
        }
        s = s + "    }\n"
        s = s + "    static fromValue(v) {\n"
        if (hasZero) {
            s = s + "        if (v == 0) return members[0]\n"
            s = s + "        if (!v.isInteger || v < 1 || v & (v-1) != 0) return null\n"
            s = s + "        var ix = (v.log / 2.log).round\n"
            s = s + "        if (ix + 1 >= members.count) return null\n"
            s = s + "        return members[ix+1]\n"
        } else {
            s = s + "        if (!v.isInteger || v < 1 || v & (v-1) != 0) return null\n"
            s = s + "        var ix = (v.log / 2.log).round\n"
            s = s + "        if (ix >= members.count) return null\n"
            s = s + "        return members[ix]\n"
        }
        s = s + "    }\n}\n"
        s = s + "return %(name)"
        return Meta.compile(s).call()
     }

     // Convenience version of above method which does not have a member with a value of zero.
     static create(name, members) { create(name, members, false) }
}

/* Group creates a group of related named constants of any type.
   The group has:
   1. static property getters for each member,
   2. a static 'members' property which returns a list of its members as strings, and
   3. a static 'values' property which returns a list of their corresponding values.
*/
class Group {
    // Creates a class for the Group (with an underscore after the name) and
    // returns a reference to it.
    static create(name, members, values) {
        if (name.type != String || name == "") Fiber.abort("Name must be a non-empty string.")
        if (members.isEmpty) Fiber.abort("A group must have at least one member.")
        if (members.count != values.count) Fiber.abort("There must be as many values as members.")
        name = name +  "_"
        var s = "class %(name) {\n"
        for (i in 0...members.count) {
            var m = members[i]
            var v = values[i]
            if (v is String) {
                s = s + "    static %(m) { \"%(v)\" }\n"
            } else {
                s = s + "    static %(m) { %(v) }\n"
            }
        }
        var mems = members.map { |m| "\"%(m)\"" }.join(", ")
        var vals = values.map  { |v| (v is String) ? "\"%(v)\"" : "%(v)" }.join(", ")
        s = s + "    static members { [%(mems)] }\n"
        s = s + "    static values  { [%(vals)] }\n"
        s = s + "}\n"
        s = s + "return %(name)"
        return Meta.compile(s).call()
    }
}

/* Struct creates a structure with any number of read/write fields of any type.
   The structure has:
   1. a constructor 'new' whose parameters are the initial field values,
   2. a property getter for each field,
   3. a property setter for each field,
   4. a 'toString' method to create a string representation of the structure, and
   5. a static 'fields' property which returns a list of its fields as strings.
*/
class Struct {
    // Creates a class for the Struct (with an underscore after the name) and
    // returns a reference to it.
    static create(name, fields) {
        if (name.type != String || name == "") Fiber.abort("Name must be a non-empty string.")
        if (fields.isEmpty) Fiber.abort("A struct must have at least one field.")
        name = name +  "_"
        var s = "class %(name) {\n"
        var flds = fields.map { |f| "\"%(f)\"" }.join(", ")
        s = s + "    static fields { [%(flds)] }\n"
        s = s + "    construct new(%(fields.join(", "))) {\n"
        for (i in 0...fields.count) {
            var f = fields[i]
            s = s + "        _%(f) = %(f)\n"
        }
        s = s + "    }\n"
        s = s + fields.map { |f| "    %(f) { _%(f) }" }.join("\n") + "\n"
        s = s + fields.map { |f| "    %(f)=(v) { _%(f) = v }" }.join("\n") + "\n"
        s = s + "    toString { \"(" + fields.map { |f| "\%(_%(f))" }.join(", ") + ")\" }\n}\n"
        s = s + "return %(name)"
        return Meta.compile(s).call()
     }
}

/* Tuple creates a tuple with any number of read-only fields of any type.
   The tuple has:
   1. a constructor 'new' whose parameters are the field values,
   2. a property getter for each field,
   3. a 'toString' method to create a string representation of the tuple, and
   4. a static 'fields' property which returns a list of its fields as strings.
*/
class Tuple {
    // Creates a class for the Tuple (with an underscore after the name) and
    // returns a reference to it.
    static create(name, fields) {
        if (name.type != String || name == "") Fiber.abort("Name must be a non-empty string.")
        if (fields.isEmpty) Fiber.abort("A tuple must have at least one field.")
        name = name +  "_"
        var s = "class %(name) {\n"
        var flds = fields.map { |f| "\"%(f)\"" }.join(", ")
        s = s + "    static fields { [%(flds)] }\n"
        s = s + "    construct new(%(fields.join(", "))) {\n"
        for (i in 0...fields.count) {
            var f = fields[i]
            s = s + "        _%(f) = %(f)\n"
        }
        s = s + "    }\n"
        s = s + fields.map { |f| "    %(f) { _%(f) }" }.join("\n") + "\n"
        s = s + "    toString { \"(" + fields.map { |f| "\%(_%(f))" }.join(", ") + ")\" }\n}\n"
        s = s + "return %(name)"
        return Meta.compile(s).call()
     }
}

/* Union creates a union whose read/write value must be a value of a given list of types.
   The union has:
   1. a constructor 'new' whose parameter is the initial value,
   2. a property getter for the value,
   3. a property setter for the value,
   4. a property getter for the kind (i.e. type) of the current value
   5. a 'toString' method to create a string representation of the current value, and
   6. a static 'types' property which returns a list of its allowable types.
*/
class Union {
    // Creates a class for the Union (with an underscore after the name) and
    // returns a reference to it.
    static create(name, types) {
        if (name.type != String || name == "") Fiber.abort("Name must be a non-empty string.")
        if (types.isEmpty) Fiber.abort("A union must have at least one type.")
        name = name + "_"
        var s =
"class %(name) {
    static types { %(types) }
    construct new(value) {
        if (!%(name).types.contains(value.type)) Fiber.abort(\"Invalid type.\")
        _value = value
    }
    value { _value }
    value=(v) {
        if (!%(name).types.contains(v.type)) Fiber.abort(\"Invalid type.\")
        _value = v
    }
    kind { _value.type }
    toString { _value.toString }
}
"
        s = s + "return %(name)"
        return Meta.compile(s).call()
    }
}

/* The following enums generate the ASCII codes for digits, upper and lower case letters. */

var Digits = Enum.create(
    "Digits", 
    ["zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"],
    48
)

var Upper = Enum.create(
    "Upper",
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toList,
    65
)

var Lower = Enum.create(
    "Lower",
    "abcdefghijklmnopqrstuvwxyz".toList,
    97
)