paul@810 | 1 | = Design Decisions = |
paul@810 | 2 | |
paul@810 | 3 | The Lichen language design involves some different choices to those taken in Python's design. Many of these choices are motivated by the following criteria: |
paul@810 | 4 | |
paul@810 | 5 | * To simplify the language and to make what programs do easier to understand and to predict |
paul@810 | 6 | * To make analysis of programs easier, particularly [[../Deduction|deductions]] about the nature of the code |
paul@810 | 7 | * To simplify and otherwise reduce the [[../Representations|representations]] employed and the operations performed at run-time |
paul@810 | 8 | |
paul@810 | 9 | Lichen is in many ways a restricted form of Python. In particular, restrictions on the attribute names supported by each object help to clearly define the object types in a program, allowing us to identify those objects when they are used. Consequently, optimisations that can be employed in a Lichen program become possible in situations where they would have been difficult or demanding to employ in a Python program. |
paul@810 | 10 | |
paul@810 | 11 | Some design choices evoke memories of earlier forms of Python. Removing nested scopes simplifies the [[../Inspection|inspection]] of programs and run-time [[../Representations|representations]] and mechanisms. Other choices seek to remedy difficult or defective aspects of Python, notably the behaviour of Python's [[../Imports|import]] system. |
paul@810 | 12 | |
paul@810 | 13 | <<TableOfContents(2,3)>> |
paul@810 | 14 | |
paul@810 | 15 | == Attributes == |
paul@810 | 16 | |
paul@810 | 17 | {{{#!table |
paul@810 | 18 | '''Lichen''' || '''Python''' || '''Rationale''' |
paul@810 | 19 | == |
paul@810 | 20 | Objects have a fixed set of attribute names |
paul@810 | 21 | || Objects can gain and lose attributes at run-time |
paul@810 | 22 | || Having fixed sets of attributes helps identify object types |
paul@810 | 23 | == |
paul@810 | 24 | Instance attributes may not shadow class attributes |
paul@810 | 25 | || Instance attributes may shadow class attributes |
paul@810 | 26 | || Forbidding shadowing simplifies access operations |
paul@810 | 27 | == |
paul@810 | 28 | Attributes are simple members of object structures |
paul@810 | 29 | || Dynamic handling and computation of attributes is supported |
paul@810 | 30 | || Forbidding dynamic attributes simplifies access operations |
paul@810 | 31 | }}} |
paul@810 | 32 | |
paul@810 | 33 | === Fixed Attribute Names === |
paul@810 | 34 | |
paul@810 | 35 | Attribute names are bound for classes through assignment in the class namespace, for modules in the module namespace, and for instances in methods through assignment to `self`. Class and instance attributes are propagated to descendant classes and instances of descendant classes respectively. Once bound, attributes can be modified, but new attributes cannot be bound by other means, such as the assignment of an attribute to an arbitrary object that would not already support such an attribute. |
paul@810 | 36 | |
paul@810 | 37 | {{{#!python numbers=disable |
paul@810 | 38 | class C: |
paul@810 | 39 | a = 123 |
paul@810 | 40 | def __init__(self): |
paul@810 | 41 | self.x = 234 |
paul@810 | 42 | |
paul@810 | 43 | C.b = 456 # not allowed (b not bound in C) |
paul@810 | 44 | C().y = 567 # not allowed (y not bound for C instances) |
paul@810 | 45 | }}} |
paul@810 | 46 | |
paul@810 | 47 | Permitting the addition of attributes to objects would then require that such addition attempts be associated with particular objects, leading to a potentially iterative process involving object type deduction and modification, also causing imprecise results. |
paul@810 | 48 | |
paul@810 | 49 | === No Shadowing === |
paul@810 | 50 | |
paul@810 | 51 | Instances may not define attributes that are provided by classes. |
paul@810 | 52 | |
paul@810 | 53 | {{{#!python numbers=disable |
paul@810 | 54 | class C: |
paul@810 | 55 | a = 123 |
paul@810 | 56 | def shadow(self): |
paul@810 | 57 | self.a = 234 # not allowed (attribute shadows class attribute) |
paul@810 | 58 | }}} |
paul@810 | 59 | |
paul@810 | 60 | Permitting this would oblige instances to support attributes that, when missing, are provided by consulting their classes but, when not missing, may also be provided directly by the instances themselves. |
paul@810 | 61 | |
paul@810 | 62 | === No Dynamic Attributes === |
paul@810 | 63 | |
paul@810 | 64 | Instance attributes cannot be provided dynamically, such that any missing attribute would be supplied by a special method call to determine the attribute's presence and to retrieve its value. |
paul@810 | 65 | |
paul@810 | 66 | {{{#!python numbers=disable |
paul@810 | 67 | class C: |
paul@810 | 68 | def __getattr__(self, name): # not supported |
paul@810 | 69 | if name == "missing": |
paul@810 | 70 | return 123 |
paul@810 | 71 | }}} |
paul@810 | 72 | |
paul@810 | 73 | Permitting this would require object types to potentially support any attribute, undermining attempts to use attributes to identify objects. |
paul@810 | 74 | |
paul@810 | 75 | == Naming == |
paul@810 | 76 | |
paul@810 | 77 | {{{#!table |
paul@810 | 78 | '''Lichen''' || '''Python''' || '''Rationale''' |
paul@810 | 79 | == |
paul@810 | 80 | Names may be local, global or built-in: nested namespaces must be initialised explicitly |
paul@810 | 81 | || Names may also be non-local, permitting closures |
paul@810 | 82 | || Limited name scoping simplifies program inspection and run-time mechanisms |
paul@810 | 83 | == |
paul@810 | 84 | `self` is a reserved name and is optional in method parameter lists |
paul@810 | 85 | || `self` is a naming convention, but the first method parameter must always refer to the accessed object |
paul@810 | 86 | || Reserving `self` assists deduction; making it optional is a consequence of the method binding behaviour |
paul@810 | 87 | }}} |
paul@810 | 88 | |
paul@810 | 89 | === Traditional Local, Global and Built-In Scopes Only === |
paul@810 | 90 | |
paul@810 | 91 | Namespaces reside within a hierarchy within modules: classes containing classes or functions; functions containing other functions. Built-in names are exposed in all namespaces, global names are defined at the module level and are exposed in all namespaces within the module, locals are confined to the namespace in which they are defined. |
paul@810 | 92 | |
paul@810 | 93 | However, locals are not inherited by namespaces from surrounding or enclosing namespaces. |
paul@810 | 94 | |
paul@810 | 95 | {{{#!python numbers=disable |
paul@810 | 96 | def f(x): |
paul@810 | 97 | def g(y): |
paul@810 | 98 | return x + y # not permitted: x is not inherited from f in Lichen (it is in Python) |
paul@810 | 99 | return g |
paul@810 | 100 | |
paul@810 | 101 | def h(x): |
paul@810 | 102 | def i(y, x=x): # x is initialised but held in the namespace of i |
paul@810 | 103 | return x + y # succeeds: x is defined |
paul@810 | 104 | return i |
paul@810 | 105 | }}} |
paul@810 | 106 | |
paul@810 | 107 | Needing to access outer namespaces in order to access any referenced names complicates the way in which such dynamic namespaces would need to be managed. Although the default initialisation technique demonstrated above could be automated, explicit initialisation makes programs easier to follow and avoids mistakes involving globals having the same name. |
paul@810 | 108 | |
paul@810 | 109 | === Reserved Self === |
paul@810 | 110 | |
paul@810 | 111 | The `self` name can be omitted in method signatures, but in methods it is always initialised to the instance on which the method is operating. |
paul@810 | 112 | |
paul@810 | 113 | {{{#!python numbers=disable |
paul@810 | 114 | class C: |
paul@810 | 115 | def f(y): # y is not the instance |
paul@810 | 116 | self.x = y # self is the instance |
paul@810 | 117 | }}} |
paul@810 | 118 | |
paul@810 | 119 | The assumption in methods is that `self` must always be referring to an instance of the containing class or of a descendant class. This means that `self` cannot be initialised to another kind of value, which Python permits through the explicit invocation of a method with the inclusion of the affected instance as the first argument. Consequently, `self` becomes optional in the signature because it is not assigned in the same way as the other parameters. |
paul@810 | 120 | |
paul@810 | 121 | == Inheritance and Binding == |
paul@810 | 122 | |
paul@810 | 123 | {{{#!table |
paul@810 | 124 | '''Lichen''' || '''Python''' || '''Rationale''' |
paul@810 | 125 | == |
paul@810 | 126 | Class attributes are propagated to class hierarchy members during initialisation: rebinding class attributes does not affect descendant class attributes |
paul@810 | 127 | || Class attributes are propagated live to class hierarchy members and must be looked up by the run-time system if not provided by a given class |
paul@810 | 128 | || Initialisation-time propagation simplifies access operations and attribute table storage |
paul@810 | 129 | == |
paul@810 | 130 | Unbound methods must be bound using a special function taking an instance |
paul@810 | 131 | || Unbound methods may be called using an instance as first argument |
paul@810 | 132 | || Forbidding instances as first arguments simplifies the invocation mechanism |
paul@810 | 133 | == |
paul@810 | 134 | Functions assigned to class attributes do not become unbound methods |
paul@810 | 135 | || Functions assigned to class attributes become unbound methods |
paul@810 | 136 | || Removing method assignment simplifies deduction: methods are always defined in place |
paul@810 | 137 | == |
paul@810 | 138 | Base classes must be well-defined |
paul@810 | 139 | || Base classes may be expressions |
paul@810 | 140 | || Well-defined base classes are required to establish a well-defined hierarchy of types |
paul@810 | 141 | == |
paul@810 | 142 | Classes may not be defined in functions |
paul@810 | 143 | || Classes may be defined in any kind of namespace |
paul@810 | 144 | || Forbidding classes in functions prevents the definition of countless class variants that are awkward to analyse |
paul@810 | 145 | }}} |
paul@810 | 146 | |
paul@810 | 147 | === Inherited Class Attributes === |
paul@810 | 148 | |
paul@810 | 149 | Class attributes that are changed for a class do not change for that class's descendants. |
paul@810 | 150 | |
paul@810 | 151 | {{{#!python numbers=disable |
paul@810 | 152 | class C: |
paul@810 | 153 | a = 123 |
paul@810 | 154 | |
paul@810 | 155 | class D(C): |
paul@810 | 156 | pass |
paul@810 | 157 | |
paul@810 | 158 | C.a = 456 |
paul@810 | 159 | print D.a # remains 123 in Lichen, becomes 456 in Python |
paul@810 | 160 | }}} |
paul@810 | 161 | |
paul@810 | 162 | Permitting this requires indirection for all class attributes, requiring them to be treated differently from other kinds of attributes. Meanwhile, class attribute rebinding and the accessing of inherited attributes changed in this way is relatively rare. |
paul@810 | 163 | |
paul@810 | 164 | === Unbound Methods === |
paul@810 | 165 | |
paul@810 | 166 | Methods are defined on classes but are only available via instances: they are instance methods. Consequently, acquiring a method directly from a class and then invoking it should fail because the method will be unbound: the "context" of the method is not an instance. Furthermore, the Python technique of supplying an instance as the first argument in an invocation to bind the method to an instance, thus setting the context of the method, is not supported. See [[#ReservedSelf|"Reserved Self"]] for more information. |
paul@810 | 167 | |
paul@810 | 168 | {{{#!python numbers=disable |
paul@810 | 169 | class C: |
paul@810 | 170 | def f(self, x): |
paul@810 | 171 | self.x = x |
paul@810 | 172 | def g(self): |
paul@810 | 173 | C.f(123) # not permitted: C is not an instance |
paul@810 | 174 | C.f(self, 123) # not permitted: self cannot be specified in the argument list |
paul@810 | 175 | get_using(C.f, self)(123) # binds C.f to self, then the result is called |
paul@810 | 176 | }}} |
paul@810 | 177 | |
paul@810 | 178 | Binding methods to instances occurs when acquiring methods via instances or explicitly using the `get_using` built-in. The built-in checks the compatibility of the supplied method and instance. If compatible, it provides the bound method as its result. |
paul@810 | 179 | |
paul@810 | 180 | Normal functions are callable without any further preparation, whereas unbound methods need the binding step to be performed and are not immediately callable. Were functions to become unbound methods upon assignment to a class attribute, they would need to be invalidated by having the preparation mechanism enabled on them. However, this invalidation would only be relevant to the specific case of assigning functions to classes and this would need to be tested for. Given the added complications, such functionality is arguably not worth supporting. |
paul@810 | 181 | |
paul@810 | 182 | === Assigning Functions to Class Attributes === |
paul@810 | 183 | |
paul@810 | 184 | Functions can be assigned to class attributes but do not become unbound methods as a result. |
paul@810 | 185 | |
paul@810 | 186 | {{{#!python numbers=disable |
paul@810 | 187 | class C: |
paul@810 | 188 | def f(self): # will be replaced |
paul@810 | 189 | return 234 |
paul@810 | 190 | |
paul@810 | 191 | def f(self): |
paul@810 | 192 | return self |
paul@810 | 193 | |
paul@810 | 194 | C.f = f # makes C.f a function, not a method |
paul@810 | 195 | C().f() # not permitted: f requires an explicit argument |
paul@810 | 196 | C().f(123) # permitted: f has merely been exposed via C.f |
paul@810 | 197 | }}} |
paul@810 | 198 | |
paul@810 | 199 | Methods are identified as such by their definition location, they contribute information about attributes to the class hierarchy, and they employ certain structure details at run-time to permit the binding of methods. Since functions can defined in arbitrary locations, no class hierarchy information is available, and a function could combine `self` with a range of attributes that are not compatible with any class to which the function might be assigned. |
paul@810 | 200 | |
paul@810 | 201 | === Well-Defined Base Classes === |
paul@810 | 202 | |
paul@810 | 203 | Base classes must be clearly identifiable as well-defined classes. This facilitates the cataloguing of program objects and further analysis on them. |
paul@810 | 204 | |
paul@810 | 205 | {{{#!python numbers=disable |
paul@810 | 206 | class C: |
paul@810 | 207 | x = 123 |
paul@810 | 208 | |
paul@810 | 209 | def f(): |
paul@810 | 210 | return C |
paul@810 | 211 | |
paul@810 | 212 | class D(f()): # not permitted: f could return anything |
paul@810 | 213 | pass |
paul@810 | 214 | }}} |
paul@810 | 215 | |
paul@810 | 216 | If base class identification could only be done reliably at run-time, class relationship information would be very limited without running the program or performing costly and potentially unreliable analysis. Indeed, programs employing such dynamic base classes are arguably resistant to analysis, which is contrary to the goals of a language like Lichen. |
paul@810 | 217 | |
paul@810 | 218 | === Class Definitions and Functions === |
paul@810 | 219 | |
paul@810 | 220 | Classes may not be defined in functions because functions provide dynamic namespaces, but Lichen relies on a static namespace hierarchy in order to clearly identify the principal objects in a program. If classes could be defined in functions, despite seemingly providing the same class over and over again on every invocation, a family of classes would, in fact, be defined. |
paul@810 | 221 | |
paul@810 | 222 | {{{#!python numbers=disable |
paul@810 | 223 | def f(x): |
paul@810 | 224 | class C: # not permitted: this describes one of potentially many classes |
paul@810 | 225 | y = x |
paul@810 | 226 | return f |
paul@810 | 227 | }}} |
paul@810 | 228 | |
paul@810 | 229 | Moreover, issues of namespace nesting also arise, since the motivation for defining classes in functions would surely be to take advantage of local state to parameterise such classes. |
paul@810 | 230 | |
paul@810 | 231 | == Modules and Packages == |
paul@810 | 232 | |
paul@810 | 233 | {{{#!table |
paul@810 | 234 | '''Lichen''' || '''Python''' || '''Rationale''' |
paul@810 | 235 | == |
paul@810 | 236 | Modules are independent: package hierarchies are not traversed when importing |
paul@810 | 237 | || Modules exist in hierarchical namespaces: package roots must be imported before importing specific submodules |
paul@810 | 238 | || Eliminating module traversal permits more precise imports and reduces superfluous code |
paul@810 | 239 | == |
paul@810 | 240 | Only specific names can be imported from a module or package using the `from` statement |
paul@810 | 241 | || Importing "all" from a package or module is permitted |
paul@810 | 242 | || Eliminating "all" imports simplifies the task of determining where names in use have come from |
paul@810 | 243 | == |
paul@810 | 244 | Modules must be specified using absolute names |
paul@810 | 245 | || Imports can be absolute or relative |
paul@810 | 246 | || Using only absolute names simplifies the import mechanism |
paul@810 | 247 | == |
paul@810 | 248 | Modules are imported independently and their dependencies subsequently resolved |
paul@810 | 249 | || Modules are imported as import statements are encountered |
paul@810 | 250 | || Statically-initialised objects can be used declaratively, although an initialisation order may still need establishing |
paul@810 | 251 | }}} |
paul@810 | 252 | |
paul@810 | 253 | === Independent Modules === |
paul@810 | 254 | |
paul@810 | 255 | The inclusion of modules in a program affects only explicitly-named modules: they do not have relationships implied by their naming that would cause such related modules to be included in a program. |
paul@810 | 256 | |
paul@810 | 257 | {{{#!python numbers=disable |
paul@810 | 258 | from compiler import consts # defines consts |
paul@810 | 259 | import compiler.ast # defines ast, not compiler |
paul@810 | 260 | |
paul@810 | 261 | ast # is defined |
paul@810 | 262 | compiler # is not defined |
paul@810 | 263 | consts # is defined |
paul@810 | 264 | }}} |
paul@810 | 265 | |
paul@810 | 266 | Where modules should have relationships, they should be explicitly defined using `from` and `import` statements which target the exact modules required. In the above example, `compiler` is not routinely imported because modules within the `compiler` package have been requested. |
paul@810 | 267 | |
paul@810 | 268 | === Specific Name Imports Only === |
paul@810 | 269 | |
paul@810 | 270 | Lichen, unlike Python, also does not support the special `__all__` module attribute. |
paul@810 | 271 | |
paul@810 | 272 | {{{#!python numbers=disable |
paul@810 | 273 | from compiler import * # not permitted |
paul@810 | 274 | from compiler import ast, consts # permitted |
paul@810 | 275 | |
paul@810 | 276 | interpreter # undefined in compiler (yet it might be thought to reside there) and in this module |
paul@810 | 277 | }}} |
paul@810 | 278 | |
paul@810 | 279 | The `__all__` attribute supports `from ... import *` statements in Python, but without identifying the module or package involved and then consulting `__all__` in that module or package to discover which names might be involved (which might require the inspection of yet other modules or packages), the names imported cannot be known. Consequently, some names used elsewhere in the module performing the import might be assumed to be imported names when, in fact, they are unknown in both the importing and imported modules. Such uncertainty hinders the inspection of individual modules. |
paul@810 | 280 | |
paul@810 | 281 | === Modules Imported Independently === |
paul@810 | 282 | |
paul@810 | 283 | When indicating an import using the `from` and `import` statements, the [[../Toolchain|toolchain]] does not attempt to immediately import other modules. Instead, the imports act as declarations of such other modules or names from other modules, resolved at a later stage. This permits mutual imports to a greater extent than in Python. |
paul@810 | 284 | |
paul@810 | 285 | {{{#!python numbers=disable |
paul@810 | 286 | # Module M |
paul@810 | 287 | from N import C # in Python: fails attempting to re-enter N |
paul@810 | 288 | |
paul@810 | 289 | class D(C): |
paul@810 | 290 | y = 456 |
paul@810 | 291 | |
paul@810 | 292 | # Module N |
paul@810 | 293 | from M import D # in Python: causes M to be entered, fails when re-entered from N |
paul@810 | 294 | |
paul@810 | 295 | class C: |
paul@810 | 296 | x = 123 |
paul@810 | 297 | |
paul@810 | 298 | class E(D): |
paul@810 | 299 | z = 789 |
paul@810 | 300 | |
paul@810 | 301 | # Main program |
paul@810 | 302 | import N |
paul@810 | 303 | }}} |
paul@810 | 304 | |
paul@810 | 305 | Such flexibility is not usually needed, and circular importing usually indicates issues with program organisation. However, declarative imports can help to decouple modules and avoid combining import declaration and module initialisation order concerns. |
paul@810 | 306 | |
paul@810 | 307 | == Syntax and Control-Flow == |
paul@810 | 308 | |
paul@810 | 309 | {{{#!table |
paul@810 | 310 | '''Lichen''' || '''Python''' || '''Rationale''' |
paul@810 | 311 | == |
paul@810 | 312 | If expressions and comprehensions are not supported |
paul@810 | 313 | || If expressions and comprehensions are supported |
paul@810 | 314 | || Omitting such syntactic features simplifies program inspection and translation |
paul@810 | 315 | == |
paul@810 | 316 | The `with` statement is not supported |
paul@810 | 317 | || The `with` statement offers a mechanism for resource allocation and deallocation using context managers |
paul@810 | 318 | || This syntactic feature can be satisfactorily emulated using existing constructs |
paul@810 | 319 | == |
paul@810 | 320 | Generators are not supported |
paul@810 | 321 | || Generators are supported |
paul@810 | 322 | || Omitting generator support simplifies run-time mechanisms |
paul@810 | 323 | == |
paul@810 | 324 | Only positional and keyword arguments are supported |
paul@810 | 325 | || Argument unpacking (using `*` and `**`) is supported |
paul@810 | 326 | || Omitting unpacking simplifies generic invocation handling |
paul@810 | 327 | == |
paul@810 | 328 | All parameters must be specified |
paul@810 | 329 | || Catch-all parameters (`*` and `**`) are supported |
paul@810 | 330 | || Omitting catch-all parameter population simplifies generic invocation handling |
paul@810 | 331 | }}} |
paul@810 | 332 | |
paul@810 | 333 | === No If Expressions or Comprehensions === |
paul@810 | 334 | |
paul@810 | 335 | In order to support the classic [[WikiPedia:?:|ternary operator]], a construct was [[https://www.python.org/dev/peps/pep-0308/|added]] to the Python syntax that needed to avoid problems with the existing grammar and notation. Unfortunately, it reorders the components from the traditional form: |
paul@810 | 336 | |
paul@810 | 337 | {{{#!python numbers=disable |
paul@810 | 338 | # Not valid in Lichen, only in Python. |
paul@810 | 339 | |
paul@810 | 340 | # In C: condition ? true_result : false_result |
paul@810 | 341 | true_result if condition else false_result |
paul@810 | 342 | |
paul@810 | 343 | # In C: (condition ? inner_true_result : inner_false_result) ? true_result : false_result |
paul@810 | 344 | true_result if (inner_true_result if condition else inner_false_result) else false_result |
paul@810 | 345 | }}} |
paul@810 | 346 | |
paul@810 | 347 | Since if expressions may participate within expressions, they cannot be rewritten as if statements. Nor can they be rewritten as logical operator chains in general. |
paul@810 | 348 | |
paul@810 | 349 | {{{#!python numbers=disable |
paul@810 | 350 | # Not valid in Lichen, only in Python. |
paul@810 | 351 | |
paul@810 | 352 | a = 0 if x else 1 # x being true yields 0 |
paul@810 | 353 | |
paul@810 | 354 | # Here, x being true causes (x and 0) to complete, yielding 0. |
paul@810 | 355 | # But this causes ((x and 0) or 1) to complete, yielding 1. |
paul@810 | 356 | |
paul@810 | 357 | a = x and 0 or 1 # not valid |
paul@810 | 358 | }}} |
paul@810 | 359 | |
paul@810 | 360 | But in any case, it would be more of a motivation to support the functionality if a better syntax could be adopted instead. However, if expressions are not particularly important in Python, and despite enhancement requests over many years, everybody managed to live without them. |
paul@810 | 361 | |
paul@810 | 362 | List and generator comprehensions are more complicated but share some characteristics of if expressions: their syntax contradicts the typical conventions established by the rest of the Python language; they create implicit state that is perhaps most appropriately modelled by a separate function or similar object. Since Lichen does not support generators at all, it will obviously not support generator expressions. |
paul@810 | 363 | |
paul@810 | 364 | Meanwhile, list comprehensions quickly encourage barely-readable programs: |
paul@810 | 365 | |
paul@810 | 366 | {{{#!python numbers=disable |
paul@810 | 367 | # Not valid in Lichen, only in Python. |
paul@810 | 368 | |
paul@810 | 369 | x = [0, [1, 2, 0], 0, 0, [0, 3, 4]] |
paul@810 | 370 | a = [z for y in x if y for z in y if z] |
paul@810 | 371 | }}} |
paul@810 | 372 | |
paul@810 | 373 | Supporting the creation of temporary functions to produce list comprehensions, while also hiding temporary names from the enclosing scope, adds complexity to the toolchain for situations where programmers would arguably be better creating their own functions and thus writing more readable programs. |
paul@810 | 374 | |
paul@810 | 375 | === No With Statement === |
paul@810 | 376 | |
paul@810 | 377 | The [[https://docs.python.org/2.7/reference/compound_stmts.html#the-with-statement|with statement]] introduced the concept of [[https://docs.python.org/2.7/reference/datamodel.html#context-managers|context managers]] in Python 2.5, with such objects supporting a [[https://docs.python.org/2.7/library/stdtypes.html#typecontextmanager|programming interface]] that aims to formalise certain conventions around resource management. For example: |
paul@810 | 378 | |
paul@810 | 379 | {{{#!python numbers=disable |
paul@810 | 380 | # Not valid in Lichen, only in Python. |
paul@810 | 381 | |
paul@810 | 382 | with connection = db.connect(connection_args): |
paul@810 | 383 | with cursor = connection.cursor(): |
paul@810 | 384 | cursor.execute(query, args) |
paul@810 | 385 | }}} |
paul@810 | 386 | |
paul@810 | 387 | Although this makes for readable code, it must be supported by objects which define the `__enter__` and `__exit__` special methods. Here, the `connect` method invoked in the first `with` statement must return such an object; similarly, the `cursor` method must also provide an object with such characteristics. |
paul@810 | 388 | |
paul@810 | 389 | However, the "pre-with" solution is as follows: |
paul@810 | 390 | |
paul@810 | 391 | {{{#!python numbers=disable |
paul@810 | 392 | connection = db.connect(connection_args) |
paul@810 | 393 | try: |
paul@810 | 394 | cursor = connection.cursor() |
paul@810 | 395 | try: |
paul@810 | 396 | cursor.execute(query, args) |
paul@810 | 397 | finally: |
paul@810 | 398 | cursor.close() |
paul@810 | 399 | finally: |
paul@810 | 400 | connection.close() |
paul@810 | 401 | }}} |
paul@810 | 402 | |
paul@810 | 403 | Although this seems less readable, its behaviour is more obvious because magic methods are not being called implicitly. Moreover, any parameterisation of the acts of resource deallocation or closure can be done in the `finally` clauses where such parameterisation would seem natural, rather than being specified through some kind of context manager initialisation arguments that must then be propagated to the magic methods so that they may take into consideration contextual information that is readily available in the place where the actual resource operations are being performed. |
paul@810 | 404 | |
paul@810 | 405 | === No Generators === |
paul@810 | 406 | |
paul@810 | 407 | [[https://www.python.org/dev/peps/pep-0255/|Generators]] were [[https://docs.python.org/release/2.3/whatsnew/section-generators.html|added]] to Python in the 2.2 release and became fully part of the language in the 2.3 release. They offer a convenient way of writing iterator-like objects, capturing execution state instead of obliging the programmer to manage such state explicitly. |
paul@810 | 408 | |
paul@810 | 409 | {{{#!python numbers=disable |
paul@810 | 410 | # Not valid in Lichen, only in Python. |
paul@810 | 411 | |
paul@810 | 412 | def fib(): |
paul@810 | 413 | a, b = 0, 1 |
paul@810 | 414 | while 1: |
paul@810 | 415 | yield b |
paul@810 | 416 | a, b = b, a+b |
paul@810 | 417 | |
paul@810 | 418 | # Alternative form valid in Lichen. |
paul@810 | 419 | |
paul@810 | 420 | class fib: |
paul@810 | 421 | def __init__(self): |
paul@810 | 422 | self.a, self.b = 0, 1 |
paul@810 | 423 | |
paul@810 | 424 | def next(self): |
paul@810 | 425 | result = self.b |
paul@810 | 426 | self.a, self.b = self.b, self.a + self.b |
paul@810 | 427 | return result |
paul@810 | 428 | |
paul@810 | 429 | # Main program. |
paul@810 | 430 | |
paul@810 | 431 | seq = fib() |
paul@810 | 432 | i = 0 |
paul@810 | 433 | while i < 10: |
paul@810 | 434 | print seq.next() |
paul@810 | 435 | i += 1 |
paul@810 | 436 | }}} |
paul@810 | 437 | |
paul@810 | 438 | However, generators make additional demands on the mechanisms provided to support program execution. The encapsulation of the above example generator in a separate class illustrates the need for state that persists outside the execution of the routine providing the generator's results. Generators may look like functions, but they do not necessarily behave like them, leading to potential misunderstandings about their operation even if the code is superficially tidy and concise. |
paul@810 | 439 | |
paul@810 | 440 | === Positional and Keyword Arguments Only === |
paul@810 | 441 | |
paul@810 | 442 | When invoking callables, only positional arguments and keyword arguments can be used. Python also supports `*` and `**` arguments which respectively unpack sequences and mappings into the argument list, filling the list with sequence items (using `*`) and keywords (using `**`). |
paul@810 | 443 | |
paul@810 | 444 | {{{#!python numbers=disable |
paul@810 | 445 | def f(a, b, c, d): |
paul@810 | 446 | return a + b + c + d |
paul@810 | 447 | |
paul@810 | 448 | l = range(0, 4) |
paul@810 | 449 | f(*l) # not permitted |
paul@810 | 450 | |
paul@810 | 451 | m = {"c" : 10, "d" : 20} |
paul@810 | 452 | f(2, 4, **m) # not permitted |
paul@810 | 453 | }}} |
paul@810 | 454 | |
paul@810 | 455 | While convenient, such "unpacking" arguments obscure the communication between callables and undermine the safety provided by function and method signatures. They also require run-time support for the unpacking operations. |
paul@810 | 456 | |
paul@810 | 457 | === Positional Parameters Only === |
paul@810 | 458 | |
paul@810 | 459 | Similarly, signatures may only contain named parameters that correspond to arguments. Python supports `*` and `**` in parameter lists, too, which respectively accumulate superfluous positional and keyword arguments. |
paul@810 | 460 | |
paul@810 | 461 | {{{#!python numbers=disable |
paul@810 | 462 | def f(a, b, *args, **kw): # not permitted |
paul@810 | 463 | return a + b + sum(args) + kw.get("c", 0) + kw.get("d", 0) |
paul@810 | 464 | |
paul@810 | 465 | f(1, 2, 3, 4) |
paul@810 | 466 | f(1, 2, c=3, d=4) |
paul@810 | 467 | }}} |
paul@810 | 468 | |
paul@810 | 469 | Such accumulation parameters can be useful for collecting arbitrary data and applying some of it within a callable. However, they can easily proliferate throughout a system and allow erroneous data to propagate far from its origin because such parameters permit the deferral of validation until the data needs to be accessed. Again, run-time support is required to marshal arguments into the appropriate parameter of this nature, but programmers could just write functions and methods that employ general sequence and mapping parameters explicitly instead. |