123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736 |
- *vim9class.txt* For Vim version 9.0. Last change: 2022 Dec 11
- 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
- < *object* *Object*
- 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})'
- < *E1317* *E1327*
- 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* *Class* *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.
- A class can only be defined in a |Vim9| script file. *E1316*
- A class cannot be defined inside a function.
- 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.
- *E1315*
- 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 its interface, the object members and methods, with a
- named interface. This avoids the need for separately specifying the
- interface, which is often done in many languages, especially Java.
- Items in a class ~
- *E1318* *E1325* *E1326*
- Inside a class, in betweeen `:class` and `:endclass`, these items can appear:
- - An object member declaration: >
- this._memberName: memberType
- this.memberName: memberType
- public this.memberName: memberType
- - A constructor method: >
- def new(arguments)
- def newName(arguments)
- - An object method: >
- def SomeMethod(arguments)
- 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 = v:none, this.age = v:none, this.gender = v:none)
- enddef
- All object members will be used, also private access ones.
- The "= v:none" default values make the arguments optional. Thus you can also
- call `new()` without any arguments. No assignment will happen and the default
- value for the object members will be used. This is a more useful example,
- with default values: >
- class TextPosition
- this.lnum: number = 1
- this.col: number = 1
- endclass
- If you want the constructor to have mandatory arguments, you need to write it
- yourself. For example, if for the AutoNew class above you insist on getting
- the name, you can define the constructor like this: >
- def new(this.name, this.age = v:none, this.gender = v:none)
- enddef
- < *E1328*
- Note that you cannot use another default value than "v:none" here. If you
- want to initialize the object members, do it where they are declared. This
- way you only need to look in one place for the default values.
- 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 members 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:
|