diff options
author | Bram Moolenaar <Bram@vim.org> | 2022-12-04 20:13:24 +0000 |
---|---|---|
committer | Bram Moolenaar <Bram@vim.org> | 2022-12-04 20:13:24 +0000 |
commit | c1c365c1ca4881488c4fc419b4d5e579b89ef2ed (patch) | |
tree | d32cb77aace1caf8261fa347a4cebe8ae45cc8d2 /runtime/doc | |
parent | b21b8e9ed081a6ef6b6745fe65d219b3ac046c3b (diff) | |
download | vim-git-c1c365c1ca4881488c4fc419b4d5e579b89ef2ed.tar.gz |
patch 9.0.1001: classes are not documented or implemented yetv9.0.1001
Problem: Classes are not documented or implemented yet.
Solution: Make the first steps at documenting Vim9 objects, classes and
interfaces. Make initial choices for the syntax. Add a skeleton
implementation. Add "public" and "this" in the command table.
Diffstat (limited to 'runtime/doc')
-rw-r--r-- | runtime/doc/Makefile | 2 | ||||
-rw-r--r-- | runtime/doc/vim9.txt | 93 | ||||
-rw-r--r-- | runtime/doc/vim9class.txt | 697 |
3 files changed, 711 insertions, 81 deletions
diff --git a/runtime/doc/Makefile b/runtime/doc/Makefile index 578679971..2ac337a5a 100644 --- a/runtime/doc/Makefile +++ b/runtime/doc/Makefile @@ -161,6 +161,7 @@ DOCS = \ version9.txt \ vi_diff.txt \ vim9.txt \ + vim9class.txt \ visual.txt \ windows.txt \ workshop.txt @@ -313,6 +314,7 @@ HTMLS = \ vi_diff.html \ vimindex.html \ vim9.html \ + vim9class.html \ visual.html \ windows.html \ workshop.html diff --git a/runtime/doc/vim9.txt b/runtime/doc/vim9.txt index d7f78087a..1b1298cd8 100644 --- a/runtime/doc/vim9.txt +++ b/runtime/doc/vim9.txt @@ -16,7 +16,7 @@ features in Vim9 script. 3. New style functions |fast-functions| 4. Types |vim9-types| 5. Namespace, Import and Export |vim9script| -6. Future work: classes |vim9-classes| +6. Classes and interfaces |vim9-classes| 9. Rationale |vim9-rationale| @@ -1940,73 +1940,17 @@ Or: > ============================================================================== -6. Future work: classes *vim9-classes* - -Above "class" was mentioned a few times, but it has not been implemented yet. -Most of Vim9 script can be created without this functionality, and since -implementing classes is going to be a lot of work, it is left for the future. -For now we'll just make sure classes can be added later. - -Thoughts: -- `class` / `endclass`, the whole class must be in one file -- Class names are always CamelCase (to avoid a name clash with builtin types) -- A single constructor called "constructor" (similar to TypeScript) -- Single inheritance: `class ThisClass extends BaseClass` -- `interface` / `endinterface` (looks like a class without any implementation) -- Explicit declaration that the class supports an interface, so that type - checking works properly: - `class SomeClass implements SomeInterface, OtherInterface` -- `abstract class` (class with incomplete implementation) - not really needed? -- Class (static) methods and Object methods: syntax to be defined. -- Class (static) members and Object members: syntax to be defined. -- Access control: private / protected / shared / public ? Keep it simple. -- Access object members with `this.member` ? -- Generics for class: `class <Tkey, Tentry>` -- Generics for function: `def <Tkey> GetLast(key: Tkey)` -- Method overloading (two methods with the same name but different argument - types): Most likely not -- Mixins: not sure if that is useful, leave out for simplicity. - -Again, much of this is from TypeScript with a slightly different syntax. - -Some things that look like good additions: -- Use a class as an interface (like Dart) -- Extend a class with methods, using an import (like Dart) -- Mixins -- For testing: Mock mechanism - -An important class that will be provided is "Promise". Since Vim is single -threaded, connecting asynchronous operations is a natural way of allowing -plugins to do their work without blocking the user. It's a uniform way to -invoke callbacks and handle timeouts and errors. - -Some commands have already been reserved: - *:class* - *:endclass* - *:abstract* - *:enum* - *:endenum* - *:interface* - *:endinterface* - *:static* - *:type* - -Some examples: > - - abstract class Person - static const prefix = 'xxx' - var name: string - - def constructor(name: string) - this.name = name - enddef - - def display(): void - echo name - enddef - - abstract def find(string): Person - endclass +6. Classes and interfaces *vim9-classes* + +In legacy script a Dictionary could be used as a kind-of object, by adding +members that are functions. However, this is quite inefficient and requires +the writer to do the work of making sure all the objects have the right +members. See |Dictionary-function|. + +In |Vim9| script you can have classes, objects and interfaces like in most +popular object-oriented programming languages. Since this is a lot of +functionality it is located in a separate help file: |vim9class.txt|. + ============================================================================== @@ -2293,18 +2237,5 @@ tool need to be supported. Since most languages support classes the lack of support for classes in Vim is then a problem. -Classes ~ - -Vim supports a kind-of object oriented programming by adding methods to a -dictionary. With some care this can be made to work, but it does not look -like real classes. On top of that, it's quite slow, because of the use of -dictionaries. - -It would be good to support real classes, and this is planned for a later -version. The support is a "minimal common functionality" of class support in -most languages. It will work much like Java, which is the most popular -programming language. - - vim:tw=78:ts=8:noet:ft=help:norl: diff --git a/runtime/doc/vim9class.txt b/runtime/doc/vim9class.txt new file mode 100644 index 000000000..cb86b4d13 --- /dev/null +++ b/runtime/doc/vim9class.txt @@ -0,0 +1,697 @@ +*vim9class.txt* For Vim version 9.0. Last change: 2022 Dec 04 + + + VIM REFERENCE MANUAL by Bram Moolenaar + + +NOTE - This is under development, anything can still change! - NOTE + + +Vim9 classes, objects, interfaces, types and enums. + +1. Overview |Vim9-class-overview| +2. A simple class |Vim9-simple-class| +3. Using an abstract class |Vim9-abstract-class| +4. Using an interface |Vim9-using-interface| +5. More class details |Vim9-class| +6. Type definition |Vim9-type| +7. Enum |Vim9-enum| + +9. Rationale +10. To be done later + +============================================================================== + +1. Overview *Vim9-class-overview* + +The fancy term is "object-oriented programming". You can find lots of study +material about this subject. Here we document what |Vim9| script provides, +assuming you know the basics already. Added are helpful hints about how +to use this functionality effectively. + +The basic item is an object: +- An object stores state. It contains one or more variables that can each + have a value. +- An object usually provides functions that manipulate its state. These + functions are invoked "on the object", which is what sets it apart from the + traditional separation of data and code that manipulates the data. +- An object has a well defined interface, with typed member variables and + member functions. +- Objects are created by a class and all objects have the same interface. + This never changes, it is not dynamic. + +An object can only be created by a class. A class provides: +- A new() method, the constructor, which returns an object for the class. + This method is invoked on the class name: MyClass.new(). +- State shared by all objects of the class: class variables and constants. +- A hierarchy of classes, with super-classes and sub-classes, inheritance. + +An interface is used to specify properties of an object: +- An object can declare several interfaces that it implements. +- Different objects implementing the same interface can be used the same way. + +The class hierarchy allows for single inheritance. Otherwise interfaces are +to be used where needed. + + +Class modeling ~ + +You can model classes any way you like. Keep in mind what you are building, +don't try to model the real world. This can be confusing, especially because +teachers use real-world objects to explain class relations and you might think +your model should therefore reflect the real world. It doesn't! The model +should match your purpose. + +You will soon find that composition is often better than inheritance. Don't +waste time trying to find the optimal class model. Or waste time discussing +whether a square is a rectangle or that a rectangle is a square. It doesn't +matter. + + +============================================================================== + +2. A simple class *Vim9-simple-class* + +Let's start with a simple example: a class that stores a text position: > + + class TextPosition + this.lnum: number + this.col: number + + def new(lnum: number, col: number) + this.lnum = lnum + this.col = col + enddef + + def SetLnum(lnum: number) + this.lnum = lnum + enddef + + def SetCol(col: number) + this.col = col + enddef + + def SetPosition(lnum: number, col: number) + this.lnum = lnum + this.col = col + enddef + endclass + +You can create an object from this class with the new() method: > + + var pos = TextPosition.new(1, 1) + +The object members "lnum" and "col" can be accessed directly: > + + echo $'The text position is ({pos.lnum}, {pos.col})' + +If you have been using other object-oriented languages you will notice that +in Vim the object members are consistently referred to with the "this." +prefix. This is different from languages like Java and TypeScript. This +naming convention makes the object members easy to spot. Also, when a +variable does not have the "this." prefix you know it is not an object member. + + +Member write access ~ + +Now try to change an object member directly: > + + pos.lnum = 9 + +This will give you an error! That is because by default object members can be +read but not set. That's why the class provides a method for it: > + + pos.SetLnum(9) + +Allowing to read but not set an object member is the most common and safest +way. Most often there is no problem using a value, while setting a value may +have side effects that need to be taken care of. In this case, the SetLnum() +method could check if the line number is valid and either give an error or use +the closest valid value. + +If you don't care about side effects and want to allow the object member to be +changed at any time, you can make it public: > + + public this.lnum: number + public this.col number + +Now you don't need the SetLnum(), SetCol() and SetPosition() methods, setting +"pos.lnum" directly above will no longer give an error. + + +Private members ~ + +On the other hand, if you do not want the object members to be read directly, +you can make them private. This is done by prefixing an underscore to the +name: > + + this._lnum: number + this._col number + +Now you need to provide methods to get the value of the private members. +These are commonly call getters. We recommend using a name that starts with +"Get": > + + def GetLnum(): number + return this._lnum + enddef + + def GetCol() number + return this._col + enddef + +This example isn't very useful, the members might as well have been public. +It does become useful if you check the value. For example, restrict the line +number to the total number of lines: > + + def GetLnum(): number + if this._lnum > this._lineCount + return this._lineCount + endif + return this._lnum + enddef + + +Simplifying the new() method ~ + +Many constructors take values for the object members. Thus you very often see +this pattern: > + + this.lnum: number + this.col: number + + def new(lnum: number, col: number) + this.lnum = lnum + this.col = col + enddef + +Not only is this text you need to write, it also has the type of each member +twice. Since this is so common a shorter way to write new() is provided: > + + def new(this.lnum, this.col) + enddef + +The semantics are easy to understand: Providing the object member name, +including "this.", as the argument to new() means the value provided in the +new() call is assigned to that object member. This mechanism is coming from +the Dart language. + +The sequence of constructing a new object is: +1. Memory is allocated and cleared. All values are zero/false/empty. +2. For each declared member that has an initializer, the expression is + evaluated and assigned to the member. This happens in the sequence the + members are declared in the class. +3. Arguments in the new() method in the "this.name" form are assigned. +4. The body of the new() method is executed. + +TODO: for a sub-class the constructor of the parent class will be invoked +somewhere. + + +============================================================================== + +3. Using an abstract class *Vim9-abstract-class* + +An abstract class forms the base for at least one sub-class. In the class +model one often finds that a few classes have the same properties that can be +shared, but a class with those properties does not have enough state to create +an object from. A sub-class must extend the abstract class and add the +missing state and/or methods before it can be used to create objects for. + +An abstract class does not have a new() method. + +For example, a Shape class could store a color and thickness. You cannot +create a Shape object, it is missing the information about what kind of shape +it is. The Shape class functions as the base for a Square and a Triangle +class, for which objects can be created. Example: > + + abstract class Shape + this.color = Color.Black + this.thickness = 10 + endclass + + class Square extends Shape + this.size: number + + def new(this.size) + enddef + endclass + + class Triangle extends Shape + this.base: number + this.height: number + + def new(this.base, this.height) + enddef + endclass +< + *class-member* *:static* +Class members are declared with "static". They are used by the name without a +prefix: > + + class OtherThing + this.size: number + static totalSize: number + + def new(this.size) + totalSize += this.size + enddef + endclass +< + *class-method* +Class methods are also declared with "static". They have no access to object +members, they cannot use the "this" keyword. > + + class OtherThing + this.size: number + static totalSize: number + + " Clear the total size and return the value it had before. + static def ClearTotalSize(): number + var prev = totalSize + totalSize = 0 + return prev + enddef + endclass + + +============================================================================== + +4. Using an interface *Vim9-using-interface* + +The example above with Shape, Square and Triangle can be made more useful if +we add a method to compute the surface of the object. For that we create the +interface called HasSurface, which specifies one method Surface() that returns +a number. This example extends the one above: > + + abstract class Shape + this.color = Color.Black + this.thickness = 10 + endclass + + interface HasSurface + def Surface(): number + endinterface + + class Square extends Shape implements HasSurface + this.size: number + + def new(this.size) + enddef + + def Surface(): number + return this.size * this.size + enddef + endclass + + class Triangle extends Shape implements HasSurface + this.base: number + this.height: number + + def new(this.base, this.height) + enddef + + def Surface(): number + return this.base * this.height / 2 + enddef + endclass + +The interface name can be used as a type: > + + var shapes: list<HasSurface> = [ + Square.new(12), + Triangle.new(8, 15), + ] + for shape in shapes + echo $'the surface is {shape.Surface()}' + endfor + + +============================================================================== + +5. More class details *Vim9-class* + +Defining a class ~ + *:class* *:endclass* *:abstract* +A class is defined between `:class` and `:endclass`. The whole class is +defined in one script file. It is not possible to add to a class later. + +It is possible to define more than one class in a script file. Although it +usually is better to export only one main class. It can be useful to define +types, enums and helper classes though. + +The `:abstract` keyword may be prefixed and `:export` may be used. That gives +these variants: > + + class ClassName + endclass + + export class ClassName + endclass + + abstract class ClassName + endclass + + export abstract class ClassName + endclass +< + *E1314* +The class name should be CamelCased. It must start with an uppercase letter. +That avoids clashing with builtin types. + +After the class name these optional items can be used. Each can appear only +once. They can appear in any order, although this order is recommended: > + extends ClassName + implements InterfaceName, OtherInterface + specifies SomeInterface +< *extends* +A class can extend one other class. + *implements* +A class can implement one or more interfaces. + *specifies* +A class can declare it's interface, the object members and methods, with a +named interface. This avoids the need for separately specifying the +interface, which is often done an many languages, especially Java. + + +Defining an interface ~ + *:interface* *:endinterface* +An interface is defined between `:interface` and `:endinterface`. It may be +prefixed with `:export`: > + + interface InterfaceName + endinterface + + export interface InterfaceName + endinterface + +An interface can declare object members, just like in a class but without any +initializer. + +An interface can declare methods with `:def`, including the arguments and +return type, but without the body and without `:enddef`. Example: > + + interface HasSurface + this.size: number + def Surface(): number + endinterface + +The "Has" prefix can be used to make it easier to guess this is an interface +name, with a hint about what it provides. + + +Default constructor ~ + +In case you define a class without a new() method, one will be automatically +defined. This default constructor will have arguments for all the object +members, in the order they were specified. Thus if your class looks like: > + + class AutoNew + this.name: string + this.age: number + this.gender: Gender + endclass + +Then The default constructor will be: > + + def new(this.name, this.age, this.gender) + enddef + +All object members will be used, also private access ones. + + +Multiple constructors ~ + +Normally a class has just one new() constructor. In case you find that the +constructor is often called with the same arguments you may want to simplify +your code by putting those arguments into a second constructor method. For +example, if you tend to use the color black a lot: > + + def new(this.garment, this.color, this.size) + enddef + ... + var pants = new(Garment.pants, Color.black, "XL") + var shirt = new(Garment.shirt, Color.black, "XL") + var shoes = new(Garment.shoes, Color.black, "45") + +Instead of repeating the color every time you can add a constructor that +includes it: > + + def newBlack(this.garment, this.size) + this.color = Color.black + enddef + ... + var pants = newBlack(Garment.pants, "XL") + var shirt = newBlack(Garment.shirt, "XL") + var shoes = newBlack(Garment.shoes, "9.5") + +Note that the method name must start with "new". If there is no method called +"new()" then the default constructor is added, even though there are other +constructor methods. + + +============================================================================== + +6. Type definition *Vim9-type* *:type* + +A type definition is giving a name to a type specification. For Example: > + + :type ListOfStrings list<string> + +TODO: more explanation + + +============================================================================== + +7. Enum *Vim9-enum* *:enum* *:endenum* + +An enum is a type that can have one of a list of values. Example: > + + :enum Color + White + Red + Green + Blue + Black + :endenum + +TODO: more explanation + + +============================================================================== + +9. Rationale + +Most of the choices for |Vim9| classes come from popular and recently +developed languages, such as Java, TypeScript and Dart. The syntax has been +made to fit with the way Vim script works, such as using `endclass` instead of +using curly braces around the whole class. + +Some common constructs of object-oriented languages were chosen very long ago +when this kind of programming was still new, and later found to be +sub-optimal. By this time those constructs were widely used and changing them +was not an option. In Vim we do have the freedom to make different choices, +since classes are completely new. We can make the syntax simpler and more +consistent than what "old" languages use. Without diverting too much, it +should still mostly look like what you know from existing languages. + +Some recently developed languages add all kinds of fancy features that we +don't need for Vim. But some have nice ideas that we do want to use. +Thus we end up with a base of what is common in popular languages, dropping +what looks like a bad idea, and adding some nice features that are easy to +understand. + +The main rules we use to make decisions: +- Keep it simple. +- No surprises, mostly do what other languages are doing. +- Avoid mistakes from the past. +- Avoid the need for the script writer to consult the help to understand how + things work, most things should be obvious. +- Keep it consistent. +- Aim at an average size plugin, not at a huge project. + + +Using new() for the constructor ~ + +Many languages use the class name for the constructor method. A disadvantage +is that quite often this is a long name. And when changing the class name all +constructor methods need to be renamed. Not a big deal, but still a +disadvantage. + +Other languages, such as TypeScript, use a specific name, such as +"constructor()". That seems better. However, using "new" or "new()" to +create a new object has no obvious relation with "constructor()". + +For |Vim9| script using the same method name for all constructors seemed like +the right choice, and by calling it new() the relation between the caller and +the method being called is obvious. + + +No overloading of the constructor ~ + +In Vim script, both legacy and |Vim9| script, there is no overloading of +functions. That means it is not possible to use the same function name with +different types of arguments. Therefore there also is only one new() +constructor. + +With |Vim9| script it would be possible to support overloading, since +arguments are typed. However, this gets complicated very quickly. Looking at +a new() call one has to inspect the types of the arguments to know which of +several new() methods is actually being called. And that can require +inspecting quite a bit of code. For example, if one of the arguments is the +return value of a method, you need to find that method to see what type it is +returning. + +Instead, every constructor has to have a different name, starting with "new". +That way multiple constructors with different arguments are possible, while it +is very easy to see which constructor is being used. And the type of +arguments can be properly checked. + + +No overloading of methods ~ + +Same reasoning as for the constructor: It is often not obvious what type +arguments have, which would make it difficult to figure out what method is +actually being called. Better just give the methods a different name, then +type checking will make sure it works as you intended. This rules out +polymorphism, which we don't really need anyway. + + +Using "this.member" everywhere ~ + +The object members in various programming languages can often be accessed in +different ways, depending on the location. Sometimes "this." has to be +prepended to avoid ambiguity. They are usually declared without "this.". +That is quite inconsistent and sometimes confusing. + +A very common issue is that in the constructor the arguments use the same name +as the object member. Then for these members "this." needs to be prefixed in +the body, while for other members this is not needed and often omitted. This +leads to a mix of members with and without "this.", which is inconsistent. + +For |Vim9| classes the "this." prefix is always used. Also for declaring the +members. Simple and consistent. When looking at the code inside a class it's +also directly clear which variable references are object members and which +aren't. + + +Single inheritance and interfaces ~ + +Some languages support multiple inheritance. Although that can be useful in +some cases, it makes the rules of how a class works quite complicated. +Instead, using interfaces to declare what is supported is much simpler. The +very popular Java language does it this way, and it should be good enough for +Vim. The "keep it simple" rule applies here. + +Explicitly declaring that a class supports an interface makes it easy to see +what a class is intended for. It also makes it possible to do proper type +checking. When an interface is changed any class that declares to implement +it will be checked if that change was also changed. The mechanism to assume a +class implements an interface just because the methods happen to match is +brittle and leads to obscure problems, let's not do that. + + +Using class members ~ + +Using "static member" to declare a class member is very common, nothing new +here. In |Vim9| script these can be accessed directly by their name. Very +much like how a script-local variable can be used in a function. Since object +members are always accessed with "this." prepended, it's also quickly clear +what kind of member it is. + +TypeScript prepends the class name before the class member, also inside the +class. This has two problems: The class name can be rather long, taking up +quite a bit of space, and when the class is renamed all these places need to +be changed too. + + +Using "ClassName.new()" to construct an object ~ + +Many languages use the "new" operator to create an object, which is actually +kind of strange, since the constructor is defined as a method with arguments, +not a command. TypeScript also has the "new" keyword, but the method is +called "constructor()", it is hard to see the relation between the two. + +In |Vim9| script the constructor method is called new(), and it is invoked as +new(), simple and straightforward. Other languages use "new ClassName()", +while there is no ClassName() method, it's a method by another name in the +class called ClassName. Quite confusing. + + +Default read access to object members ~ + +Some users will remark that the access rules for object members are +asymmetric. Well, that is intentional. Changing a value is a very different +action than reading a value. The read operation has no side effects, it can +be done any number of times without affecting the object. Changing the value +can have many side effects, and even have a ripple effect, affecting other +objects. + +When adding object members one usually doesn't think much about this, just get +the type right. And normally the values are set in the new() method. +Therefore defaulting to read access only "just works" in most cases. And when +directly writing you get an error, which makes you wonder if you actually want +to allow that. This helps writing code with fewer mistakes. + + +Making object membes private with an underscore ~ + +When an object member is private, it can only be read and changed inside the +class (and in sub-classes), then it cannot be used outside of the class. +Prepending an underscore is a simple way to make that visible. Various +programming languages have this as a recommendation. + +In case you change your mind and want to make the object member accessible +outside of the class, you will have to remove the underscore everywhere. +Since the name only appears in the class (and sub-classes) they will be easy +to find and change. + +The other way around is much harder: you can easily prepend an underscore to +the object member inside the class to make it private, but any usage elsewhere +you will have to track down and change. You may have to make it a "set" +method call. This reflects the real world problem that taking away access +requires work to be done for all places where that access exists. + +An alternative would have been using the "private" keyword, just like "public" +changes the access in the other direction. Well, that's just to reduce the +number of keywords. + + +No protected object members ~ + +Some languages provide several ways to control access to object members. The +most known is "protected", and the meaning varies from language to language. +Others are "shared", "private" and even "friend". + +These rules make life more difficult. That can be justified in projects where +many people work on the same, complex code where it is easy to make mistakes. +Especially when refactoring or other changes to the class model. + +The Vim scripts are expected to be used in a plugin, with just one person or a +small team working on it. Complex rules then only make it more complicated, +the extra safety provide by the rules isn't really needed. Let's just keep it +simple and not specify access details. + + +============================================================================== + +10. To be done later + +Can a newSomething() constructor invoke another constructor? If yes, what are +the restrictions? + +Thoughts: +- Generics for a class: `class <Tkey, Tentry>` +- Generics for a function: `def <Tkey> GetLast(key: Tkey)` +- Mixins: not sure if that is useful, leave out for simplicity. + +Some things that look like good additions: +- For testing: Mock mechanism + +An important class to be provided is "Promise". Since Vim is single +threaded, connecting asynchronous operations is a natural way of allowing +plugins to do their work without blocking the user. It's a uniform way to +invoke callbacks and handle timeouts and errors. + + + vim:tw=78:ts=8:noet:ft=help:norl: |