LotusScript
May. 5th, 2011 11:43 pmMy word LotusScript is full of annoying quirks. Then again, so is every language, but returning to LS (as a hobbyist) after all these years, I can really see why people who are used to other languages find this one so annoying. It's just similar enough so you think you know what you need to do, but until you get used to it, you don't.
I just finished some code that I wrote most of last week but was too sick to want to finish until now. Of course, I was slowed down by a constant argument in my head between writing and documenting code that I'll only have to modify slightly for later aspects of the project one the one hand and making it as short and specific as possible on the other. I mostly chose to make it either general or easily generalizable but to leave out documentation of anything that will be clear to me later, as it's highly unlikely anyone else will ever have to look at the code (unless you click below). For myself, I'd rather see more lines of actual code on the screen rather than have screen real estate taken up by English.
The project is to make an online reference site for complex games, with Imperialism II being my first go. It has technologies, usually with one or more prerequisite technologies needed before you can research the one you're looking up. The code (called an Agent) I wrote takes all the technologies (stored in Notes Documents) and their immediate prerequisites (which I already manually put on the documents in a multi-value field) and calculates lists of all prerequisites, direct antecedents, and all antecedents and puts them in three more multi-value fields on each document.
Of course, LotusScript isn't really native to Notes/Domino, despite that being it's major use. For instance, in Notes, there's no difference between a scalar value and a list--a scalar is simply a list with one element. (The Notes UI can make a distinction, but the back end does not.) In LotusScript, there are arrays and scalars. (There's also something called a List, but that's another animal--an associative array with strings as keys.) And the collections returned by the Notes/Domino class library are not passable to the ForAll iterator used on collections native to LotusScript. They do come with their own iteration methods, at least.
Sometimes it pays to use the Evaluate method, which lets you pass a string to the Notes Formula language compiler. That language is beautifully integrated with the platform, but isn't rich enough to do most jobs that call for iteration or recursion.
This code itself is written in the form of an Agent, which is a Domino "design element". You can think of it as a containing class, in that it provides appropriate scope for variables and methods that you'll want to access from more than one place and Initialize and Terminate methods that will automatically run if they have any code in them.
Ten years ago, I was a proponent of not giving elements of collections ordinal numbers (by placing then in compiler-friendly arrays) unless those ordinal numbers actually meant something or the performance hit of using other kinds of collections was significant. I have a hunch now I wouldn't have to push for that because it's what everyone does, but I don't know. In any case, I make extensive use of those Lists I mentioned above, because aside from Arrays, they're the only native collection LotusScript has. Which brings up another irksome quirk...
Variables can be undeclared (but not if you're smart enough to use Option Declare), declared as a specific data type, or declared as a Variant, which means at least you have to spell it right, but there's no compile-time type checking. Unfortunately, Arrays and Lists can neither be passed nor accepted by user-written methods when declared as such. Library functions do oft-times take and/or return them, and there's no problem with Elements of collections, but you can't assign a whole collection to a variable declared to be of any type other than Variant. So I use variants a lot and comment the type they're supposed to hold.
Anyway, if you've read this far, I'd love any comments you might have on the above or on the code.
I just finished some code that I wrote most of last week but was too sick to want to finish until now. Of course, I was slowed down by a constant argument in my head between writing and documenting code that I'll only have to modify slightly for later aspects of the project one the one hand and making it as short and specific as possible on the other. I mostly chose to make it either general or easily generalizable but to leave out documentation of anything that will be clear to me later, as it's highly unlikely anyone else will ever have to look at the code (unless you click below). For myself, I'd rather see more lines of actual code on the screen rather than have screen real estate taken up by English.
The project is to make an online reference site for complex games, with Imperialism II being my first go. It has technologies, usually with one or more prerequisite technologies needed before you can research the one you're looking up. The code (called an Agent) I wrote takes all the technologies (stored in Notes Documents) and their immediate prerequisites (which I already manually put on the documents in a multi-value field) and calculates lists of all prerequisites, direct antecedents, and all antecedents and puts them in three more multi-value fields on each document.
Of course, LotusScript isn't really native to Notes/Domino, despite that being it's major use. For instance, in Notes, there's no difference between a scalar value and a list--a scalar is simply a list with one element. (The Notes UI can make a distinction, but the back end does not.) In LotusScript, there are arrays and scalars. (There's also something called a List, but that's another animal--an associative array with strings as keys.) And the collections returned by the Notes/Domino class library are not passable to the ForAll iterator used on collections native to LotusScript. They do come with their own iteration methods, at least.
Sometimes it pays to use the Evaluate method, which lets you pass a string to the Notes Formula language compiler. That language is beautifully integrated with the platform, but isn't rich enough to do most jobs that call for iteration or recursion.
This code itself is written in the form of an Agent, which is a Domino "design element". You can think of it as a containing class, in that it provides appropriate scope for variables and methods that you'll want to access from more than one place and Initialize and Terminate methods that will automatically run if they have any code in them.
Ten years ago, I was a proponent of not giving elements of collections ordinal numbers (by placing then in compiler-friendly arrays) unless those ordinal numbers actually meant something or the performance hit of using other kinds of collections was significant. I have a hunch now I wouldn't have to push for that because it's what everyone does, but I don't know. In any case, I make extensive use of those Lists I mentioned above, because aside from Arrays, they're the only native collection LotusScript has. Which brings up another irksome quirk...
Variables can be undeclared (but not if you're smart enough to use Option Declare), declared as a specific data type, or declared as a Variant, which means at least you have to spell it right, but there's no compile-time type checking. Unfortunately, Arrays and Lists can neither be passed nor accepted by user-written methods when declared as such. Library functions do oft-times take and/or return them, and there's no problem with Elements of collections, but you can't assign a whole collection to a variable declared to be of any type other than Variant. So I use variants a lot and comment the type they're supposed to hold.
Anyway, if you've read this far, I'd love any comments you might have on the above or on the code.
Option Public
Option Declare
Dim db As NotesDatabase
Dim LevelTechs(0 To 7) As NotesDocumentCollection
Dim AllPrereqs As TechList
Dim DirectPrereqs As TechList
Dim AllDependants As TechList
Dim DirectDependants As TechList
Class TechList
Public FieldName As String
Public Techs List As Variant ' String() or String
Sub New (Fname As String)
FieldName = Fname
End Sub
Sub MakeReverse (BaseList As TechList)
Dim BaseTech As string
ForAll FocusSet In BaseList.Techs ' String() or String
BaseTech = ListTag(FocusSet)
If IsArray(FocusSet) Then
ForAll FocusTech In FocusSet
AddOrMake BaseTech, FocusTech
End ForAll
Else
AddOrMake BaseTech, FocusSet
End If
End ForAll
End Sub
Sub AddOrMake (NewVal As String, Tag As Variant) 'Tag is String
Dim StartArray(1 To 2) As String
If Not IsElement(Techs(Tag)) Then
Techs(Tag) = NewVal
ElseIf IsArray(Techs(Tag)) Then
Techs(Tag) = ArrayAppend(Techs(Tag), NewVal)
Else
StartArray(1) = Techs(Tag)
StartArray(2) = NewVal
Techs(Tag) = StartArray
End If
End Sub
Sub MakeField (Doc As NotesDocument)
Const MACRO = "@SetField(DeleteThis; @Sort(@Unique(@GetField(DeleteThis))))"
REM "DeleteThis" is explained below.
Dim FocusTech As String
FocusTech = Doc.GetItemValue("Name")(0)
If IsElement(Techs(FocusTech)) Then
Doc.ReplaceItemValue FieldName, Techs(FocusTech)
Doc.ReplaceItemValue "DeleteThis", FieldName
REM The field DeleteThis contains the name of the real field to be accessed.
REM This allows MACRO to be pre-compiled, which speeds execution.
Evaluate MACRO, Doc
Doc.RemoveItem("DeleteThis")
Else
Doc.ReplaceItemValue FieldName, "none"
End If
End Sub
End Class
Sub Initialize
Dim s As New NotesSession
Set db = s.CurrentDatabase
SetLevelTechs
SupressNone
FindPrereqs
Set AllDependants = New TechList("AllDependants")
AllDependants.MakeReverse AllPrereqs
Set DirectDependants = New Techlist("DirectDependants")
DirectDependants.MakeReverse DirectPrereqs
SaveDocs
End Sub
Sub SetLevelTechs ()
Dim level As Long
Dim searchstring As String
searchstring = |Form = "Imp2Tech" & Period = |
For level = 0 To 7
Set LevelTechs(level) = db.Search(searchstring & CStr(level), nothing, 0)
Next
End Sub
Sub SupressNone
Dim Doc As NotesDocument
Set Doc = LevelTechs(0).GetFirstDocument
Do
Doc.ReplaceItemvalue "DirectPrereqs", ""
Doc.Save True, False
Set Doc = LevelTechs(0).GetNextDocument(Doc)
Loop Until Doc Is Nothing
Set Doc = LevelTechs(1).GetFirstDocument
Do
If Doc.GetItemValue("DirectPrereqs")(0) = "none" Then
Doc.ReplaceItemvalue "DirectPrereqs", ""
Doc.Save True, False
End If
Set Doc = LevelTechs(1).GetNextDocument(Doc)
Loop Until Doc Is Nothing
End Sub
Sub FindPrereqs ()
Dim FocusLevel As Long
Dim LevelColl As NotesDocumentCollection
Dim doc As NotesDocument
Dim FocusTech As String
Dim OnDocPrereqs As Variant ' String()
Dim OldTechs As Variant ' String()
Dim NewTechs As Variant ' String()
Dim StartArray (1 To 2) As string
Set DirectPrereqs = New TechList("DirectPrereqs")
Set AllPrereqs = New TechList("AllPrereqs")
For FocusLevel = 0 To 7
Set LevelColl = LevelTechs(Focuslevel)
Set doc = LevelColl.Getfirstdocument()
Do
FocusTech = doc.Getitemvalue("Name")(0)
OnDocPrereqs = doc.GetItemvalue("DirectPrereqs")
DirectPrereqs.techs(FocusTech) = OnDocPrereqs
AllPrereqs.techs(FocusTech) = OnDocPrereqs
ForAll TechName In doc.GetItemValue("DirectPrereqs") ' String
If Not TechName = "" Then
OldTechs = AllPrereqs.techs(FocusTech)
NewTechs = AllPrereqs.techs(TechName)
If IsArray(OldTechs) Then
AllPrereqs.techs(FocusTech) = ArrayAppend(OldTechs, NewTechs)
ElseIf IsArray(NewTechs) Then
AllPrereqs.techs(FocusTech) = ArrayAppend(NewTechs, OldTechs)
Else
StartArray(1) = OldTechs
StartArray(2) = NewTechs
AllPrereqs.techs(FocusTech) = StartArray
End If
End If
End ForAll
Set doc = LevelColl.Getnextdocument(doc)
Loop Until doc is Nothing
Next
End Sub
Sub SaveDocs
Dim NewFields (1 To 3) As TechList
Dim Level As Long
Dim Coll As NotesDocumentCollection
Dim Doc As NotesDocument
Dim FocusTech As string
Set NewFields(1) = AllPrereqs
Set NewFields(2) = DirectDependants
Set NewFields(3) = AllDependants
For Level = 0 To 7
Set Coll = LevelTechs(Level)
Set Doc = Coll.GetFirstDocument()
Do
ForAll TList In newFields ' TechList
TList.MakeField Doc
End ForAll
If Doc.GetitemValue("DirectPrereqs")(0) = "" Then
Doc.ReplaceItemvalue "DirectPrereqs", "none"
End If
Doc.Save True, False
Set Doc = Coll.GetNextDocument(Doc)
Loop Until Doc Is Nothing
Next
End Sub
no subject
Date: 2011-05-06 04:35 am (UTC)I'm also annoyed by mutable structures, partly because the behavior of a function on immutable structures can be read somewhat from its type signature, and I can have the compiler print out the type signature for me as a check on what I wrote (or I can code something like "print_string thing_to_check" and the type signature of the thing will be given in the resulting error message, assuming the type isn’t "string").
Loops can be annoying compared to recursion, due to their association with mutable values. Also recursions often have shorthand forms that can be written in-line as function calls, though these can be hard to keep straight.
no subject
Date: 2011-05-06 12:12 pm (UTC)no subject
Date: 2011-05-06 02:25 pm (UTC)One of the things I like about OCaml is that, when I have working code but want to rewrite a bit of it, the tendency is that the new code works the first time I succeed in compiling it. It is amazing how many programming errors are catchable by actually paying attention to types.
(The compiler has an annoying habit just to say "syntax error" for syntax errors, but this is partly because you'll probably use the camlp4 preprocessor for at least this, that it gives a more complete message on syntax errors.)
no subject
Date: 2011-05-07 07:57 pm (UTC)no subject
Date: 2011-05-08 07:48 am (UTC)Tail recursion is just about the same thing as a while-loop, with function parameters (which can be immutable) taking the place of loop variables (which cannot be immutable).
no subject
Date: 2011-05-06 04:57 am (UTC)<pre>...</pre>tags when you pasted the code into the LJ post? I know it works on LJ, because I've used it myself.no subject
Date: 2011-05-06 12:36 pm (UTC)no subject
Date: 2011-05-08 07:49 am (UTC)Well, Dvd -- glad to meet you here, and welcome your efforts...
Date: 2011-05-07 01:19 pm (UTC)I too am now a hobbist, with LotusScript -- after a 5-year break from a 15 year career as a consultant for Lotus Notes programming -- toying with the idea of re-honing my expertise with LotusScript...
So I welcome your public sharing, of your LotusScript efforts, here -- and will dig deeper through your code here, and share what I can, in further comments.
Thank you, for the fun here!
Re: Well, Dvd -- glad to meet you here, and welcome your efforts...
Date: 2011-05-07 01:38 pm (UTC)Re: Well, Dvd -- glad to meet you here, and welcome your efforts...
Date: 2011-05-08 04:31 am (UTC)