Request for Comment: cslib_class

Objects tend to have the opposite problem to inventories - there is a smaller number of objects with a greater number of fields, say 20 dragons each with 10 fields (colour, hit points, treasure, etc). In some ways this might be easier in that you only have to check for uniqueness 20 times.

Have you done any experiments with fixed-length fields? I imagine it would speed things up a lot if you could jump along the string by a fixed amount when doing your search.

1 Like

That is an excellent idea! Although it would force the writer to declare the maximum length of each field beforehand, it would remove the performance hurdle.
I will try some experiments along these lines.

Here’s a summary of what I have already implemented. To define a class, all we need is this:

*create class_dragon "name,hp,color,alignment"

The “class_” prefix is required. The default maximum length of each field is 20. If another value is needed, we can declare it this way:

*create class_dragon "name:35,hp:3,color,alignment"

The class base string will be used to control the class parameters as well as to store all instances. My perception is that this way is the easiest to use for the end user.

However, we need to create some variables to store the instances we fetch, like this:

*create currentdragon_name ""
*create currentdragon_hp 0
*create currentdragon_color ""
*create currentdragon_alignment ""

To create a new instance, this is the syntax:

*gosub_scene objects new "dragon" "John,50,blue,lawful evil"
*gosub_scene objects new "dragon" "Trevor,70,green,chaotic good"

Now we have two dragons, ready to be summoned.

If we want the “currentdragon” variable set to be filled at the same time, we can use:

*gosub_scene objects new "dragon" "John,50,blue,lawful evil" "currentdragon"

currentdragon_name == “John”
currentdragon_hp == 50
currentdragon_color == “blue”
currentdragon_alignment == “lawful evil”

We can, of course, have more than one instance active at a given time, we just need more variables…

*create myotherdragon_name ""
*create myotherdragon_hp 0
*create myotherdragon_color ""
*create myotherdragon_alignment ""

To fetch an instance, we use:

*gosub_scene objects get "dragon" "name" "Trevor" "myotherdragon"

This will fetch the first dragon named Trevor that has been stored and puts it into the “myotherdragon” variable set:

myotherdragon_name == “Trevor”
myotherdragon_hp == 70
myotherdragon_color == “green”
myotherdragon_alignment == “chaotic good”

Now they can fight each other.

Right now I am working on other methods, one to change the value of an instance’s field, another to destroy an instance, and a third to randomly return an instance.

Much of what I wrote above is compatible with the behavior you intended for cslib_class, but since I am working with a different approach for storing the instances I had to write the code from scratch.

I’ll share the code later today or tomorrow, once I have the remaining methods completed. But I wanted, for now, to know what you think about the direction this is taking. My idea is to make it as simple as possible for anyone who wants to use it.

1 Like

This is awesome.

I particularly like the way you can instantiate your data somewhere other than startup.txt - that file can get pretty crowded.

I also like that you can have more than one active object.

How do you handle it if someone tries to “set” a value too long for the field? When you instantiate it can just bug out but dynamically do you think you should bug or just truncate?

I’ll be really curious to find out how much performance is improved by the fixed-length fields.

1 Like

I like this a lot too, using strings definitely gives a lot of flexibility, albeit with a performance tradeoff.

Some specific thoughts…

I like asking the user to specify the name of the global store, and it’s a pattern already used in parts of cslib. Definitely a fan over expecting people to create a specifically named set of globals.

Something we’re missing in @StephenHart 's cslib_object is the ability to reference by IDs, which would be a lot more efficient (at least if you’ve got fixed widths in string land). Not to replace the field version, but having that as well would be good.

*gosub_scene objects new “dragon” “Trevor,70,green,chaotic good”

Do these have to be in order? Because it’s a pain to back-reference for complicated classes. A more verbose but flexible way we’ve looked at (but haven’t yet implemented) in @StephenHart 's solution is to use key-value pair parameters:

*gosub_scene objects new "dragon" "alignment" "chaotic good" "hp" 70 "name" "Trevor" "color" "green"

Where the order doesn’t matter, and for a *set routine you can specify as many or as few fields as you like.

You could do that in a string as well, but with parameters you don’t have the effort and performance hit of parsing the string.

1 Like

Right now I am throwing a *bug with a specific message. Truncating is also a good possibility, but the user may not realize the error and have a harder time noticing when the game misbehaves.

Me too! I haven’t actually done performance testing yet, but the fact that the fields are fixed-length is great. I’ve already implemented the method to return a random instance and it’s very straightforward to go to that specific position in the string and fetch the corresponding block.

I had already figured that out from the discussion and I’m thinking about it. It might make sense to have an ID field - after all we are building a database! If there is a use case that justifies it, it won’t be hard to implement. EDIT: Just implemented: get_by_index and get_by_field methods. The index is the position of the instance in the string.

I confess that I didn’t think about it. Right now the values have to be in order (although they may be empty) but you have a point.

However, if we have ten fields, we need twenty parameters for those. And I don’t know how many fields the class will have beforehand (that’s the cost of flexibility) so I wouldn’t know what to put in the *parms statement. With proper testing, I think I prefer this format:

*gosub_scene objects new "dragon" "name:Olivia,alignment:true neutral,color:orange,hp:250"

But I also thought of another possibility:

*set yetanotherdragon_name "Olivia"
*set yetanotherdragon_hp 250
*set yetanotherdragon_color "orange"
*set yetanotherdragon_alignment "true neutral"

*gosub_scene objects new "dragon" "yetanotherdragon"

Or both! What do you think?

Probably some choices/changes will eventually be made when we experience it in practice.

P.S.: One thing that will be quite difficult to implement with this approach is the inheritance relationship.

I very much agree: logic errors are much harder to detect and debug than well written runtime errors.

:+1:

If you leave *params empty you can have an arbitrary number of arguments, named param_1, param_2 etc. You can check the number of arguments with param_count.

Definitely better than nothing, but string parsing in CS is slow and expensive, even by CS standards!

This one feels quite verbose, but as you say, may be worth trying different ones in the field before jumping to conclusions!

I think that was always ambitious :’)

1 Like

@CJW Happy to do this. When I was playing around with it for my own WIP, I found I needed to create an ID field anyway. So we may as well make it compulsory.

I’ll update the current pull request.

2 Likes

This part was tricky, I had to backup the param_count variable and the first five params because when calling other subroutines these variables are overwritten. I think it would be convenient if ChoiceScript ensured that the generic parameter variables didn’t leave the scope of each subroutine.

The code got a little messy but now it works both ways:

*gosub_scene objects new “dragon” “Trevor,70,green,chaotic good”

*gosub_scene objects new "dragon" "alignment" "chaotic good" "hp" 70 "name" "Trevor" "color" "green"

I am now working on the last two commands: set and set_by_index.

1 Like

Yeah… In CSLIB we use gosub_scene to guarantee correct scoping (so you use gosub_scene even if calling a routine in the same scene). There’s a performance penalty of course, but I think it’s worth it to avoid that whole class of bugs.

1 Like

@StephenHart @CJW

Here is the code that resulted from my attempt to use strings to manage and store classes and instances (it took me a little longer than I had originally thought, not unlike any other software development project :grin:). Although a class has quite a bit more information than inventory items, the first few tests are encouraging. In one second I created fifty dragons. But I will create some more complete performance tests and share the results.

Link to objects.txt file

Description and examples
*comment ----------------------------------------------------------------------------------------------------
*comment Abstraction layer for object-based programming in ChoiceScript
*comment Version 0.1
*comment ----------------------------------------------------------------------------------------------------
*comment
*comment Description and examples:
*comment
*comment To define a class, a *create is required in startup.txt. A sorted
*comment field list must be declared in a string. Example:
*comment
*comment *create class_dragon "name,hp,color,alignment"
*comment
*comment Every time an instance is created, its fields will be stored internally
*comment in the order specified upon class creation.
*comment
*comment The "class_" prefix is required. The default maximum length of each
*comment field is 20. Both to cover the need for different sizes, as well as
*comment for maximum optimization of this module, the field size can and should
*comment be declared whenever possible like this:
*comment
*comment *create class_dragon "name:35,hp:3,color:10,alignment:15"
*comment
*comment The class string above will be used to keep the class parameters as
*comment well as to store all instances.
*comment
*comment Some variables must be created to access the contents of an instance,
*comment like in this example:
*comment
*comment *create currentdragon_name ""
*comment *create currentdragon_hp 0
*comment *create currentdragon_color ""
*comment *create currentdragon_alignment ""
*comment
*comment To create a new instance, both the following are valid:
*comment
*comment *gosub_scene objects new “dragon” “Trevor,70,green,chaotic good”
*comment *gosub_scene objects new "dragon" "alignment" "chaotic good" "hp" 70 "name" "Trevor" "color" "green"
*comment
*comment The first option must contain the field values in the order that the fields
*comment were declared when creating the class. The second option allows you to declare
*comment key-value pairs in any order.
*comment 
*comment The get command searches for the first instance that contains a match
*comment with the given field-value pair and loads the contents in the variable
*comment set which prefix is given in the fourth parameter:
*comment
*comment *gosub_scene objects get "dragon" "name" "Trevor" "currentdragon"
*comment
*comment To access an instance it is also possible to use the get_by_index command.
*comment This command returns the instance whose position in the internal storage is the
*comment number given in the second parameter.
*comment 
*comment *gosub_scene objects get_by_index "dragon" 2 "currentdragon"
*comment
*comment It is also possible to get a random instance using the get_random command:
*comment
*comment *gosub_scene objects get_random "dragon" "currentdragon"
*comment
*comment The set command finds the first instance with a given field-value pair and
*comment replace it with a new value.
*comment
*comment *gosub_scene objects set "dragon" "color" "green" "blue"
*comment 
*comment The first green dragon is now blue. The set_by_index command sets the value of
*comment a particular field in the instance whose position in the internal storage is the
*comment number given in the second parameter.
*comment
*comment *gosub_scene objects set_by_index "dragon" 2 "color" "purple"
*comment
*comment ----------------------------------------------------------------------------------------------------
*comment
*comment TO DO: 
*comment 
*comment The destroy command to delete the first instance with a given field-value pair
*comment from the class string
*comment
*comment The destroy_by_index command to delete the instance whose position in the internal
*comment storage is the number given in the second parameter.
*comment
*comment The set command should be reviewed according to its purpose.
*comment
*comment Documentation for each method
*comment
*comment Code optimization
*comment
*comment ----------------------------------------------------------------------------------------------------
Code
*label new
*params
*temp class param_1
*temp values ""
*temp pointer1 11
*temp pointer2 0
*temp maxlength 0
*temp value ""
*temp instr ""
*temp aux 0
*temp key_value true
*temp p2 param_2
*temp p3 ""
*temp p4 ""
*temp p5 ""
*temp param_number param_count
*temp myparamname ""
*temp myparamvalue ""
*if (param_number < 3)
    *set key_value false
    *set values param_2
*if (param_number > 2)
    *set p3 param_3
*if (param_number > 3)
    *set p4 param_4
    *set p5 param_5
*temp field {"class_"&class}#1
*if (field != "?")
    *gosub _init_class ("class_"&class)
*label _new_loop1
*gosub _get_next_def ("class_"&class) "pointer1" "field" "maxlength"
*if (field = "")
    *set {"class_"&class} &instr
    *return
*if (key_value = false)
    *gosub _get_next_def "values" "pointer2" "value" "aux"
    *goto _new_loop3
*set aux 0
*label _new_loop2
*set aux +2
*if (aux > param_number)
    *goto _new_loop1
*if (aux > 5)
    *set myparamname "param_"&aux
    *set myparamvalue "param_"&(aux+1)
*if (aux <= 5)
    *set myparamname "p"&aux
    *set myparamvalue "p"&(aux+1)
*if ({myparamname} != field)
    *goto _new_loop2
*set value {myparamvalue}
*label _new_loop3
*if (length(value) > maxlength)
    *bug Maximum length of ${maxlength} for field ${field} exceeded (value ${value}).
*label _new_loop4
*if (length(value) < maxlength)
    *set value value&"~"
    *goto _new_loop4
*set instr &value
*goto _new_loop1

*label get
*params class field value retval
*temp totdlen 0
*temp totrlen 0
*gosub _get_class_params {"class_"&class} "totdlen" "totrlen"
*temp pointerr (totdlen+1)
*label _get_loop1
*gosub _get_next_block ("class_"&class) "pointerr" totrlen retval
*if ({(retval&"_")&field} = value)
    *return
*if ({(retval&"_")&field} != value)
    *if (pointerr <= length({"class_"&class}))
        *goto _get_loop1
*gosub _clear_varset ("class_"&class) retval
*return

*label get_by_index
*params class index retval
*temp totdlen 0
*temp totrlen 0
*temp howmany 0
*temp pointer 0
*gosub _get_class_params {"class_"&class} "totdlen" "totrlen"
*set howmany (length({"class_"&class})-totdlen)/totrlen
*if (index > howmany)
    *gosub _clear_varset ("class_"&class) retval
    *bug Trying to fetch the instance ${index} but there are ${howmany}
*set pointer ((totdlen + ((index - 1) * totrlen)) + 1)
*gosub _get_next_block ("class_"&class) "pointer" totrlen retval
*return

*label set
*params class fieldname old new
*temp totdlen 0
*temp totrlen 0
*temp field ""
*temp maxlength 0
*temp offset 0
*temp pointer 11
*temp nextval ""
*temp index 0
*temp aux 0
*gosub _get_class_params {"class_"&class} "totdlen" "totrlen"
*label _set_loop1
*gosub _get_next_def ("class_"&class) "pointer" "field" "maxlength"
*if (field = "")
    *bug Field name ${fieldname} not found in class ${class}
*if (field != fieldname)
    *set offset +maxlength
    *goto _set_loop1
*set pointer (totdlen + offset)
*label _set_loop2
*set index +1
*if (pointer > length({"class_"&class}))
    *return
*set aux pointer
*set nextval ""
*label _set_loop3
*set aux +1
*if (aux > (pointer + maxlength))
    *goto _set_loop4
*if (({"class_"&class}#aux) = "~")
    *goto _set_loop4
*set nextval &({"class_"&class}#aux)
*goto _set_loop3
*label _set_loop4
*if (old != nextval)
    *set pointer +totrlen
    *goto _set_loop2
*gosub set_by_index class index fieldname new
*return

*label set_by_index
*params class index fieldname value
*temp totdlen 0
*temp totrlen 0
*temp field ""
*temp maxlength 0
*temp offset 0
*temp howmany 0
*temp pointer 11
*temp newdef ""
*temp aux 0
*gosub _get_class_params {"class_"&class} "totdlen" "totrlen"
*set howmany (length({"class_"&class})-totdlen)/totrlen
*if (index > howmany)
    *bug Trying to set a field in the instance ${index} but there are ${howmany}
*label _set_by_index_loop1
*gosub _get_next_def ("class_"&class) "pointer" "field" "maxlength"
*if (field = "")
    *bug Field name ${fieldname} not found in class ${class}
*if (field != fieldname)
    *set offset +maxlength
    *goto _set_by_index_loop1
*set pointer (((totdlen + ((index - 1) * totrlen)) + offset) + 1)
*set aux 0
*label _set_by_index_loop2
*set aux +1
*if (aux < pointer)
    *set newdef &({"class_"&class}#aux)
    *goto _set_by_index_loop2
*label _set_by_index_loop3
*if (length(value) < maxlength)
    *set value value&"~"
    *goto _set_by_index_loop3
*set newdef &value
*set pointer +maxlength
*label _set_by_index_loop4
*if (pointer <= length({"class_"&class}))
    *set newdef &({"class_"&class}#pointer)
    *set pointer +1
    *goto _set_by_index_loop4
*set {"class_"&class} newdef
*return

*label get_random
*params class retval
*temp totdlen 0
*temp totrlen 0
*temp howmany 0
*temp index 0
*temp pointer 0
*gosub _get_class_params {"class_"&class} "totdlen" "totrlen"
*set howmany (length({"class_"&class})-totdlen)/totrlen
*rand index 1 howmany
*set pointer ((totdlen + ((index - 1) * totrlen)) + 1)
*gosub _get_next_block ("class_"&class) "pointer" totrlen retval
*return

*comment ---------------------
*comment -- Private methods --
*comment ---------------------

*comment Initialize class
*label _init_class
*params cldef
*temp pointer 0
*temp totdlen 0
*temp totrlen 0
*temp field ""
*temp maxlength 0
*set totdlen (length({cldef}) + 13)
*label _init_class_loop1
*gosub _get_next_def cldef "pointer" "field" "maxlength"
*if (field != "")
    *set totrlen +maxlength
    *goto _init_class_loop1
*label _init_class_loop2
*if (length(totdlen) < 4)
    *set totdlen "0"&totdlen
    *goto _init_class_loop2
*label _init_class_loop3
*if (length(totrlen) < 4)
    *set totrlen "0"&totrlen
    *goto _init_class_loop3
*set {cldef} ((((("?"&totdlen)&":")&totrlen)&"#")&{cldef})&"##"
*return

*comment Fetch the next field and max length (or 0 if not defined)
*comment from a comma separated list of fields or field definition
*label _get_next_def
*params cldef pointref fieldref maxlref
*set {fieldref} ""
*set {maxlref} 0
*temp char ""
*temp isqt false
*label getnwhile
*set {pointref} +1
*if ({pointref}>length({cldef}))
    *if ({maxlref} = 0)
        *set {maxlref} 20
    *return
*set char {cldef}#{pointref}
*if ((char != ",")  and (char != "#"))
    *if (char = ":")
        *set isqt true
        *goto getnwhile
    *if (isqt)
        *set {maxlref} (({maxlref} * 10) + char)
        *goto getnwhile
    *set {fieldref} &char
    *goto getnwhile
*if ({maxlref} = 0)
    *set {maxlref} 20
*return

*comment Get class parameters (definition and record length)
*label _get_class_params
*params cldef dlen rlen
*temp pointer 1
*temp aux 0
*label _loop_get_class_params1
*set pointer +1
*if (pointer < 6)
    *set aux (cldef#pointer)
    *set {dlen} (({dlen}*10) + aux)
    *goto _loop_get_class_params1
*label _loop_get_class_params2
*set pointer +1
*if (pointer < 11)
    *set aux (cldef#pointer)
    *set {rlen} (({rlen}*10) +  aux)
    *goto _loop_get_class_params2
*return
    
*comment Fetch the next block from the instance list
*comment and load its values in a variable set
*label _get_next_block
*params cldef pointref reclength varset
*temp aux 0
*temp blockpointer 0
*temp block ""
*temp nextpointer 11
*temp nextfield ""
*temp nextlength 0
*label _loop_get_next_block1
*set aux +1
*if (aux <= reclength)
    *set block &({cldef}#{pointref})
    *set {pointref} +1
    *goto _loop_get_next_block1
*set blockpointer 1
*label _loop_get_next_block2
*gosub _get_next_def cldef "nextpointer" "nextfield" "nextlength"
*if (nextfield = "")
    *return
*set aux 0
*set {(varset&"_")&nextfield} ""
*label _loop_get_next_block3
*set aux +1
*if (aux <= nextlength)
    *if ((block#blockpointer) != "~")
        *set {(varset&"_")&nextfield} &(block#blockpointer)
    *set blockpointer +1
    *goto _loop_get_next_block3
*goto _loop_get_next_block2
*return

*comment Clear variables in a given set of field values
*label _clear_varset
*params cldef varset
*temp nextpointer 11
*temp nextfield ""
*temp nextlength 0
*label _loop_clear_varset
*gosub _get_next_def cldef "nextpointer" "nextfield" "nextlength"
*if (nextfield = "")
    *return
*set {(varset&"_")&nextfield} ""
*goto _loop_clear_varset
*return

A review/optimization and documentation of the code is needed, I will do it in the next few days. Now I’m finally going back to my WIP… and probably add some dragons to it. :slight_smile:

With a slight change, it would not be difficult to replicate this in cslib_class. For example, get_by_index could have an additional parameter for the desired prefix of an existing set of variables to keep the instance. That third parameter could even be optional (if not given, the class name is used) like this:

*comment GET_BY_INDEX
*comment ------------------------------------------------------------------
*comment Takes the instance of a class specified by the index and copies
*comment the data into the main instance
*comment ------------------------------------------------------------------
*comment
*comment params:
*comment p_classname (string): the name of the class
*comment p_index (integer): index of this class instance
*comment p_instance (string, optional): prefix of the variables for the instance copy
*comment returns:
*comment updates main class instance or a set of variables with the prefix in p_instance
*comment 
*comment usage example:
*comment cslib_class get_by_index “dragon” 2 "currentdragon"
*comment ------------------------------------------------------------------

*label get_by_index
*params
*temp p_classname param_1
*temp p_index param_2
*temp p_instance param_1
*if (param_count = 3)
    *set p_instance param_3
    
*gosub _set_instance_copy p_classname p_index p_instance
*return

(…)

*comment — ------------------------------------------------------------------
*comment — Copies the values in the instance at the given index
*comment — ------------------------------------------------------------------
*label _set_instance_copy
*params p_class p_index p_instance

*temp n 1
*temp field_count {p_class&"_proto_max"}

*label _set_copy_loop
*if (n <= field_count)
*temp field {p_class&"proto${n}"}
*set {p_instance&"${field}"} {p_class&"${p_index}${field}"}
*set n + 1
*goto _set_copy_loop

*return

That would make the following possible:

*gosub cslib_class get_by_index “dragon” 2

dragon_name == “Dave”
dragon_hp == 150

*gosub cslib_class get_by_index “dragon” 2 “mypet”

mypet_name == “Dave”
mypet_hp == 150

Just a thought.

1 Like

With a slight change, it would not be difficult to replicate this in cslib_class. For example, get_by_index could have an additional parameter for the desired prefix of an existing set of variables to keep the instance. That third parameter could even be optional (if not given, the class name is used) like this:

Seems very sensible (and awesome) to me!


It’s a little tricky to review code here, do you have it on Github or similar?

Not yet. But as soon as I finish the documentation I can put it on GitHub.

Note that the code may look cluttered and there is some degree of optimization to be done (I detected redundant blocks of code, for example). My goal was to test the possibility of using strings to store classes and instances dynamically and I think this was achieved, but I am not yet sure about the efficiency (cslib_class is much more efficient and faster for sure).

I think the current implementation of the set/set_by_field command has no relevant use cases. I don’t want to change the hitpoints of the first dragon in the list that has 100 hitpoints to 80. I want the hitpoints of that particular dragon (for example, Trevor) to go from 100 to 80, because that dragon is the one fighting a flock of rabid ducks.

The solution may go through an ID as mentioned earlier in this thread (not necessarily the position in the instance list, instead it may be a unique key such as the dragon’s name). However a new parameter is enough to make the command more viable:

*gosub_scene objects set “dragon” “name” “Trevor” “hp” “80”

That is, the first instance whose name is Trevor will now have the hp field with the value 80. If “name” were the key of the dragon class we could just require its value (Trevor), but I think the above format is more flexible and understandable.

1 Like

Makes perfect sense. @CJW We should put it on the TBD list.

(BTW cslib_class has mutated into cslib_object because it’s a bit too simple for ‘class’. cslib_class is up for grabs if you ever felt like adding your code to cslib…)

1 Like

Very much this! :slight_smile:

It would be an honor. :slight_smile:

…but while objects and classes were the motivation, I don’t think the code I wrote is flexible enough to allow users to write new methods (something that would be possible in an object-oriented approach).

Also, since the module stores and processes data, I feel it is closer to a database manager, where “new” equals “insert”, “get” equals “select” and “set” equals “update”.

As a contribution to cslib, I would propose submitting this code as a prototype for a cslib_db, thus also keeping a clear separation with the work already done by @StephenHart.

What do you think?

1 Like

You’re right - much closer to a database. Would be great to have it.

Can’t wait to see how you write your SQL engine in CS :slight_smile:

2 Likes

You could definitely write methods, but I agree that the library itself probably won’t be able to help much with that (though I’ll probably spend some extra brain cycles pondering that one some more).

I’d certainly still be happy to see other use-cases from this :slight_smile:

Pull request submitted! :partying_face:

However, I’m having some trouble turning the existing *gosub into *gosub_scene because there are variables that belong to the main functions that the helper functions must update.

There is quite a bit of work to do, which may involve rewriting code.

I want to point out the *bug statement at the beginning of each cslib file. I usually put *finish at the beginning of my auxiliary files so that I can put them in the *scene_list (I read somewhere that this has to happen for quicktest and/or randomtest to work well).


Available functions:

new - Creates a new record in a given table
get - Finds the first record that matches a given field-value pair
get_by_index - Finds a record by its position in the table
set - Updates a field in the first record that matches a given field-value pair
set_by_index - Updates a field in the record whose position in the table matches a given number
count - Counts the total number of records in a table
delete - Deletes the first record that matches a given field-value pair
delete_by_index - Deletes the record whose position in the table matches a given number

TO DO:

  • Extend the set function to update multiple fields
  • Extend the count function to work with a condition
  • Use cslib_ret to indicate the success or failure of each function.
  • Consider requiring the first field of each table to be a unique key
  • Consider changing the function names to SQL equivalents (although this is not SQL!)
  • Consider extending the set function to update all records that match the condition
  • Consider extending the delete function to erase all records that match the condition
  • …and much more :slight_smile:
2 Likes