The basic idea of ManifestTyping is that safety can be obtained via redundancy. It is: "I'll tell ya' what I'm gonna' tell ya', then I'll tell ya' what I'm tellin' ya'." The basic two-phase approach to communication is a major basis for safety; for example, one can use it as one vector to shut down many forms of code-injection ("SQL updates table X; insert into X(a,b,c) values (1,2,3),(1,2,5),(2,5,3)"). To benefit from it safety-wise, the first half ("I'll tell ya' what I'm gonna tell ya'") must not be a duplicate of the last half. Usually, this means that the first half is an abstraction of the last half - something with details missing. And that's basically what types are: a value-type, for example, is a general descriptor of a value, but with details (like exactly which value) missing. The safety is achieved when the recipient of the message is able to look at what you said you were going to say, and what you actually said, and complain: hey! wait a minute! something is wrong!
Types can do more than just provide safety. First, they can also provide context, which may (depending on the language) affect the parse. This is enhanced further if there is some reflection on the types involved, allowing a degree of type-sensitive MetaProgramming (where the meaning of an expression varies based upon the types involved). C++ gurus in design of the Boost libraries and elsewhere have utilized this to great effect with templated code and templated expressions (TemplateMetaprogramming). This is mentioned in the discussion below as part of code-generation, but any form of TypefulProgramming takes advantage of types in this manner.
Additionally, type proofs (like any proof) provide basis for safe assumption, which is the foundation of all safe optimizations. Every single type says something about what is true or what is not true of a value or expression. A heavily typed language, at least for StaticTyping or SoftTyping, has much greater potential for a great number of automated optimizations. (Of course, one needs to design an optimizer to take advantage of this potential.) People who largely use DynamicTyping languages tend to scoff at the need for optimization, but they also tend to work on systems that don't involve graphics, audio, realtime, operating systems, device drivers, middleware, etc.
In ImplicitTyping systems (including TypeInference systems), the analogy would be more akin to: "I'll try 'n discern what you're tryin' ta' tell me, then I'll look'n'see if yer' actually tellin' me." I.e. one first infers the type-context, then does pretty much the safety check or contextual interpretation as above. In this case the programmers don't experience the redundancy in any direct manner. Generally, programmers of ImplicitTyping systems still benefit from the ability to provide manifest types - both at the interfaces to their modules, and to better control the type-context as it is involved in the parse.
For parsing, code-generation, and optimization-purposes, types are certainly not redundant. And, as mentioned, the programmer isn't the entity experiencing the apparent redundancy for ImplicitTyping. So whether TypesAreRedundant depends not only on how (and whether) you are declaring them, but also on why you are applying them. And even where they are clearly redundant - which is in a small minority of cases, at least when TypefulProgramming is involved, they are likely to be providing just the benefit from redundancy that you're looking for: safety in numbers.
Of course, all that is subject to you choosing a typed language that supports ImplicitTyping (at least a minimal version of it) and TypefulProgramming. There are plenty of StaticallyTyped languages that make a nuisance of themselves, getting in your face about up-front manifest declarations of types before you can do anything (and for quick scripts with minimal up-front design, how would you know what types you want before you've done something? eh?). And those... those languages deserve a good scoffing, at least if they are 'newer' make - from the post MlLanguage/HaskellLanguage era. DynamicLanguage enthusiasts, and especially StaticTyping enthusiasts, should demand, at minimum a simple auto type for variable specifications for capturing the return-value of an expressions and for inlined functions, if not full ImplicitTyping (full ImplicitTyping also allows, essentially, auto type for function parameters - essentially templates). Even C++ looks to be gaining limited auto-typing in its next standard revision.
... And the rest of this page has a much'a'do about types and little or nothing involving redundancy.
RefactorMe: This page seems to be redundant with lots of others on Wiki; perhaps a WikiGnome with an hour or two to kill might make progress?
When does TypeChecking take place?
Strict Type Checking: (at compile time)
C++ is big on strict type checking at compile time. To make a polymorphic method call on instances of different classes, (1) the caller must use a pointer or reference to (2) a common base class among the instance's classes, which (3) declares the method/function "virtual." #2 means that classes supporting a given polymorphic method must all inherit from a common base class, which is (#1) used by their clients. The base class doesn't have to be a "mix in;" it can be an "interface class", which by convention is a class with no data members and nothing but pure virtual functions.
(See also C++ templates, under "Mixed / Code Generation" below.)
Java does strict type checking. Each class can only inherit from one other class, but can implement as many interfaces as desired. Using interfaces for polymorphism instead of inheritance will often make things easier. If inheritance is needed, try using inner classes.
Standard COM interfaces, being "just like C++," have strictly defined types on all parameters. But then COM throws in VARIANTS which are dynamically typed for all intents and purposes.
Loose Type Checking: (at run time)
Methods in the SmalltalkLanguage don't specify the types of their parameters, so there can be no "compile time type checks." Typically, errors of passing the wrong type of object are found at run time, when the offending object is unable to process method calls made to it.
Example: If you pass an "Employee" to a Payroll method that expects a "Check," the Employee instance will fail (with "#doesnotunderstand") when the Payroll method attempts a "setPaymentRecipient" method on it.
(Note: There were/are a few research projects on type checking in Smalltalk, e.g. TypedSmalltalk.)
COM IDispatch interface: (VisualBasic "as Object" or Variant declarations)
Methods are looked up at run time, and can fail at that time.
Mixed / Code Generation: (at compile time)
When you use C++ Templates, code is generated at compile time to handle the data types you use as parameters. The StandardTemplateLibrary (stl) uses this extensively.
Historically, if you use "the wrong kind of class" as a template parameter, the error messages can be pretty obscure.
Mixed / Static Code Analysis: (at compile time)
>>> Discussed below. <<<
[By MaxFriedental...]
That's the use of type-checking? When we call an objects method:
void foo (TObj obj) { obj.bar(); }the type-checking claims to guarantee us at the compile-time two following things:
Let's start with the first sentence. Consider the following C++ code:
class A { public: void bar(void); }; class B : public A { // }; class C { public: void bar(void); }; void foo (A *obj) { obj->bar(); } main () { A *a = new A; A *b = new B; C *c = new C; foo(a);//OK foo(b);//OK foo(c);//O-ops }Why does the compiler forbid using foo() with object of class C? It knows, how to call bar() for object, referenced by c, so there will be no technical problems this that at the run-time. And I don't want to inherit C from A or B. Of course, I can use multiple inheritance and conception of "mixes", but only to workaround this problem, because I end up with a bunch of mini-mix-classes, with one method per class. Thus the Right Way to solve this problem is to check by method names, not by types. For example CAML, while claiming to be strict type-checking language, allows write abovementioned code in the following way:
class a = object method bar = 0 end;; class b = object inherit a end;; class c = object method bar = 0 end;; let foo obj = obj#bar;; let a_obj = new a;; let b_obj = new b;; let c_obj = new c;; foo(a_obj);; foo(b_obj);; foo(c_obj);;But hey, where is the types here? Method foo hasn't any explicit type for its argument. Moreover, any class that implements method bar will fit for foo's argument. Is it type-checking? Well, of course it could be called special-case-type-checking. But from the viewpoint of application programmer it is none other than a method-checking. Types place too strict restrictions, and that's why I think they are redundant here. It is always sufficient to check by method signatures.
So you'll love template functions then...
template <class T> void foo (T *obj) { obj->bar(); }The above literally means "call bar in whatever type I pass in as the obj parameter".
So:
foo(a);//OK foo(b);//OK foo(c);//OK as well! class d { void bar(int x); }; foo(d); // ooops... error message will tell you that d // has no bar method that accepts zero parameters.-- DanielEarwicker. ps. StaticTypeSafety rules!
And what about second sentence? Does type-checking help to preserve semantics of the method? No, it doesn't. At least in the modern, polymorphic world. An example:
class A { protected: int i; public: virtual void bar (int a) { i += a; } }; class B : public A { public: virtual void bar (int a) { i -= a;//Ha-ha-ha } }; void foo (A *obj) { obj->bar(5); //O-ops }I know, I know, it is bad to change the semantics of a method in the derived classes. But languages permit it. Type-checking doesn't help. Thus preserving the same semantics of a method from class to class in real-world programs is just reasonable agreement of programmers, without any support from language and compiler. And of course such agreement can be done without inheritance and type-checking.
And what about types? Do they unusable? I think, they greatly fit the niche of object factories, a la SmallTalk classes. But in compile-time checking they are harmful and redundant. -- MaxFriedental
What about this PythonLanguage example?
class ClassWithBar: def hasBar(self): return 1 def bar(self): pass class ClassWithoutBar: def hasBar(self): return 0 def factory(): if makeClassWithBbar(): return ClassWithBar() else: return ClassWithoutBar() c = factory() if c.hasBar(): c.bar()You and I know this code will work, and there are arguably legitimate uses for this kind of programming, but the hypothetical compiler is going to reject it, because it knows that factory() can return instances which don't have bar(). -- WayneConrad
What if the hypothetical compiler will treat codeblock (and "suite" in Python) as anonymous function, and all visible variables inside it as parameters of this function?
c = factory()# first method-checking space if c.hasBar(): c.bar()# second method-checking spaceObject c is checked to have only hasBar() in the first space, and bar() in the second space. -- MaxFriedental
Static analysis of factory() reveals that the object it returns will certainly have hasBar(), so the compiler allows the call to c.hasBar(). However, the return value of factory() may or may not have bar(), so the compiler must flag c.bar() as a compile-time error. Although it is valid code, the compiler must reject it. -- WayneConrad
Sure, but only if we have to use "hand-written" method hasBar(), that has no semantic relationship with the presence of bar(). If the language provide means to query variable for methods it support, compile-time checking is possible. It could be like this (on pseudocode):
foo (arg) is require arg#bar()//true, if arg has method bar() do arg.bar() end. c = factory() if c#bar() then foo(c) end.Sophisticated compilers can see that foo(c) will be called only if c has method bar and therefore accept this code. --MaxFriedental
But if your code consistently checks to see if a method exists before calling it, then there's no need for a compiler to verify that the methods exist.
If I would check it every time I need to call a method, my source text become an unreadable mess. Besides, 99% of my everyday code calls methods of the known (at compile-time) classes. And it could be handy to have some compile-time checking. -- MaxFriedental
In COM, you can already do this, because when IUnknown::QueryInterface returns NULL, you know that the object does not support those methods, so you don't call them. Likewise, when a method lookup fails with COM's IDispatch, you can avoid calling it. Likewise, you can use reflection in Java to see if a method exists. And in the SmalltalkLanguage, you can process the #doesnotunderstand message that occurs when you call an undefined method. In other cases, you can probably catch the errors or exceptions that are thrown. So you don't really need any special compiler support to do what you want. -- JeffGrigg
How do you do static interface checking in a polymorphic language? A polymorphic method call can only be resolved at run time. That method implementation may require a different interface subset than another implementation. Please explain how types are redundant if you want to minimize the cost of dynamic dispatch to be a quick vector table lookup as with C++. -- SunirShah
Sunir, I think the idea is that the compiler, through FlowAnalysis?, can infer a signature for any object. That signature is not guaranteed to contain all methods that the object responds to, but it is guaranteed to contain only methods that the object responds to. The compiler can use that signature, rather than declared type, for compile-time checking. -- WayneConrad
Practical TypeInference in polymorphic languages has been an open research problem until recently. However, the approach used by OhHaskell seems highly promising. -- DavidSarahHopwood
Let me demonstrate an example of what I am thinking about, and maybe you could help me understand how the flow analysis would understand what to do.
class Foo>> >>wozzle: aThing aThing fungy. aThing fongy. << << class Bar>> >>wozzle: aThing aThing bungy. aThing bongy. << << class Wombat>> >>wizzle: aThing with: anotherThing wizzle wozzle: anotherThing. << << aWombat wizzle: aFoo with: target. aWombat wizzle: aBar with: target.How do you determine what methods target has to be capable of statically? I suppose you could run a flow analysis over the entire program, so you know which types of wizzling are doing on target. But this wouldn't work with dynamic linking or reflection, would it? Is this a correct conclusion? -- SunirShah
I believe so. Figuring out what methods are available on parameters is the spanner in the works. And, as you mention, reflection. The rest of the proposal isn't all that hard (it'd be very similar to the algorithm javac uses to figure out which local variables you've assigned to). My gut feeling is that typed-based-on-signature is somewhat less restrictive than type-based-on-class, but still prevent you from doing many useful things that Smalltalk or Python let you do. In addition, it would be a bear to compile. Reflection and being called by outside code would force you to leave large holes in the checking. -- WayneConrad
I wonder if the extra compile time justifies the extra runtime and the extra flexibility. Actually, in this case, I'd guess it would be more complex to determine "why did the compile fail?" as you have to hunt through the codebase for the offending context. A lot of "magic" happening here. -- SunirShah
I don't think so. The compiler anyway have to track path from the point of passing an argument back to the points of creation of all objects that could possible become an argument. So it could use this info to create informative diagnostic messages. -- MaxFriedental
Existing compilers don't currently track objects back to the point of creation. And, in the most general case, they can't. For any strategy you can come up with to "know" what object types are possible at a given point in the code, I can come up with a counter-example that will break it (by adding another possible type to the system, after your method is compiled). -- JeffGrigg
I mean the hypothetical compiler, that can do method-checking. And how do you possible could add new type after my method is compiled? If you mean reflection or COM, IMHO these cases can be statically checked only with native wrapper classes... -- MaxFriedental
Let's take a classic example - drawing 2D shapes: We've written some "shape" classes - Square, Rectangle and Triange, which implement methods like Draw(drawing_context), Move(deltax,deltay) and Rotate(degrees_clockwise). You wrote and compiled lots of code that uses those methods. Let's say you even put it in a library that gets linked in with my "shape" classes. That's OK, as all possible objects implement all three methods.
Now I implement a new class, Circle, and I don't bother to implement the Rotate() method, as it wouldn't do anything. I link it in with your code, and you're hosed - unless the system has some really wild dependency checking that knows to recompile your code because it "depends upon" - code which did not exist when it was compiled, and code that it does not directly reference.
OK, it tries to recompile your code. Problem: With this hypothetical compiler, your code no longer compiles because you can't be sure that the Rotate method is implemented in all object instances you might encounter. Your code didn't change, but it no longer compiles. That doesn't seem like a good thing. -- JeffGrigg
I didn't quiet understand, why my code have to be recompiled. Suppose I wrote a function FunnyShow?(shape), that moves rotating shape from one corner to another. In C++ it will be void FunnyShow? (Shape *shape);. Once compiled, it need not to be recompiled (if you place definition of Shape in the one header, and definition of Circle etc in the other). If you try to pass to this function some inappropriate object, it is your code will be blamed. The same with my hypothetical language. A list of methods required for shape is known after the compilation of FunnyShow? (shape);. When you try to pass to this function an object, the compiler find from your source code all possible classes the object may belong to, and if even in one case object may be inappropriate, it flag an error. In your code. -- MaxFriedental
So the answer is to write everything as C++ Templates. ;->
Actually, I'd recommend taking a serious look at defining the interfaces between classes as "interfaces." In C++, this would be classes with no member functions and nothing but pure virtual functions. Like...
class IPrintable { public: virtual void PrintToDevice?(DeviceContext? *dc) = 0; virtual std::string PrintToString?() = 0; virtual ...() = 0; ...etc... } ;-- JeffGrigg
Please consider the following:
class I1 { public: virtual void method1(void) = 0; }; class I2 { public: virtual void method2(void) = 0; }; class I3 { public: virtual void method3(void) = 0; }; class I12 : public I1, public I2 { }; class I13 : public I1, public I3 { }; class I23 : public I2, public I3 { }; class I123 : public I1, public I2, public I3 { }; class Obj : public I123 { public: virtual void method1(void) {...}; virtual void method2(void) {...}; virtual void method3(void) {...}; }; void foo1 (I1 *param) { param->method1(); }; void foo2 (I2 *param) { param->method2(); }; void foo3 (I3 *param) { param->method3(); }; void foo12 (I12 *param) { param->method1(); param->method2(); }; void foo13 (I13 *param) { param->method1(); param->method3(); }; void foo23 (I23 *param) { param->method2(); param->method3(); }; void foo123 (I123 *param) { param->method1(); param->method2(); param->method3(); };How should I define Obj (and/or interfaces) to be able to pass it to all the foo's? Thanks. -- MaxFriedental
Change the definition of I123 so that it inherits from I12 etc instead of I1 etc. Make your multiple inheritance virtual. With those changes, an object of type Obj* can be passed happily to all the foo methods. I have the feeling I'm missing your point, though. -- GarethMcCaughan
Thanks. I didn't know that we could do virtual inheritance in C++. About my point. We have a class with three methods, and we need to create seven more classes to cover all possibly uses of the class. If a class has n methods, we need sum[n*(n-1)*...*(n-r+1)/r!],r=1..n interfaces. For 10 methods, it would be 1023 interfaces. So the choice is: either automagically check for required method signatures, or:
I agree that having to define all the intermediate classes is grim. I'd assumed that wasn't your point since you presented a very nice example of how it's necessary but then didn't mention it. :-) -- GarethMcCaughan
Try it this way:
class IAll { public: virtual void methodAll(void) {}; }; class I1 : public IAll { public: virtual void method1(void) = 0; }; class I2 : public IAll { public: virtual void method2(void) = 0; }; class I3 : public IAll { public: virtual void method3(void) = 0; }; class Obj : public I1, I2, I3 { public: virtual void method1(void) {...}; virtual void method2(void) {...}; virtual void method3(void) {...}; }; void foo1 (I1 *param) { param->method1(); }; void foo2 (I2 *param) { param->method2(); }; void foo3 (I3 *param) { param->method3(); }; void foo12 (IAll *param) { I1 *i1 = dynamic_cast<I1 *>(param); if (i1 != NULL) i1->method1(); I2 *i2 = dynamic_cast<I2 *>(param); if (i2 != NULL) i2->method2(); }; // (Same trick for foo23, ..., foo123.) Other code: { Obj o; foo1(&o); foo12(static_cast<I1 *>(&o)); // cast needed to resolve ambiguity. };This is essentially how COM does it; with IUnknown instead of IAll, and QueryInterface instead of dynamic_cast<>. Yes, the static_cast<> is icky, but it would not be needed in practice if practically all the code used interfaces instead of direct references to classes.
-- JeffGrigg
Please, for the love of God, No! Try it this way :)
Define the functions as template functions. Then they will simply match the method names they want to call. Then there is no need for an inheritance hierarchy... they're so 1989 :)
What's the use to take a typed language and then to workaround its type-checking? (I assume here, that an ideal programmer in an ideal world isn't bound to any particular buzz-language like C/C++) -- MaxFriedental
Types are not redundant for a very good reason: encapsulation. Maybe the compiler could figure out an interface definition from a method's implementation. But then, programmers who call the method would need to read the implementation too, and the whole point of encapsulation is that the callers of a method shouldn't need to know the implementation details.
Furthermore, an interface should not depend on which methods are actually called by an implementation, but rather on which methods could potentially be called. As an example, suppose I have a sort method that always compares numbers using the less than operator ('<'). This doesn't mean that callers should be free to skip the implementation of '>' when they implement a new kind of number. If the sort method ever needs to be changed, the programmer should be free to use either '<' or '>' without code breaking because the caller was lazy.
Furthermore, an interface should not depend on which methods are actually called by an implementation, but rather on which methods could potentially be called.
Although this seems a little off topic for this page, I would argue you should limit an interface to the methods that are actually called. I've seen too many developers side tracked putting in functions and variations on functions that could be needed. These added functions just tend to give both the class/module and the function "weight" making them difficult to modify or correct. This is just a restatement of DoTheSimplestThingThatCouldPossiblyWork. Another way to state this is to rely on "white box" reuse not "black box" reuse. -- WayneMack
Brian, it is easily to predict could-be-potentially-called methods in your example. But try to predict such methods in the more general case, say, for an abovementioned FunnyShow? (Shape *shape). Will it require Blink() besides Rotate() and Move() in the future version, or it will be morphTo(Shape*), or both? -- MaxFriedental
Classes are often used in multiple contexts. Either multiple interfaces must be used, or a single interface must be the union of the requirements. The nature of the contexts will determine which approach is chosen. If the "union" approach is used, then the interface may be wider than is needed by one specific client. However, the fact that the interface is wider is not a consequence of method-splurge: its a consequence of multiple uses. And the fact that the interface is wider than is strictly necessary gives an implementer of a service the license to re-implement using the full interface, with confidence that no clients should be broken. -- DaveWhipp
Types are not redundant for a very good reason: encapsulation.
Encapsulation is a good reason, but types aren't a single way to ensure it. Smalltalkers do encapsulation without any type-checking, aren't they? And if we get really nice hypothetical IDE (not just vim+command line), it could not only figure out an interface definition, but also to show this definition to the programmer - for example, as a list of required methods for each argument. -- MaxFriedental
Remember that types are real. Within the computer each type does have its own memory representation and associated operations. When a language hides data types, it also takes responsibility for conversion among types, and must do the conversion without the concept of a context. This recently led to a lot of problems with dates and Y2K and has been an ongoing problem is some C mathematical algorithms (signed vs unsigned, int vs long). Strict type checking forces the programmer to make a decision on a data type based on its intended use. If you don't feel comfortable in defining how you are going to use a parameter, you are probably not ready to write the function. -- WayneMack
Types are real if you work on the implementation (low)level. This is good for some situations, but if you work in a high enough level of abstraction, you don't care how types are represented. You only care about the operations that they support. In fact, you actually point out the redundancy: if you know how you are going to use the parameters, and you write the functions well, why do you need to spell out the details about the parameters explicitly? Other humans should read the docs, and the compiler should infer the types by how you use them. -- AmirLivne
The difference between Java (and other BondageAndDisciplineLanguages) and C++ types is that with C++, the compiler can do all the optimizations it needs to make the code small and fast. With Java, the compiler has a very hard time doing any static analysis because the classes load dynamically. You can wrap a whole class around one byte in C++, but you can't in Java. If you think types are redundant in C++, you just don't understand the compiler. If you don't understand the underlying platform, you're really asking for trouble.
To C++ and the underlying platform they are not redundant. But it may be redundant to the people using it.
I just wrote a condemnation of the misuse of dynamic binding in every Web application environment I've ever used at http://virtualschool.edu/wap (click The Problem in the left margin) -- BradCox (Note added 11/26/2003: the wap project has been replaced by http://virtualschool.edu/java+ and http://virtualschool.edu/jwaa, and the rant replaced by a distinction between flexibility and stiffness in all of the above. Flexible vs stiff is an important engineering distinction (why else would we need Viagra?) that should NEVER be glossed over through the knee-jerk preference for "flexible" endemic in application server literature and all too many OO arguments. Stiffness is the ability to detect errors at compile time. Flexibility is the ability to change things at run-time, which also means errors can persist until then too. This is precisely why I prefer environments that provide both (i.e. Objective-C, java+, jwaa)... the ability to choose the right tool for the job.
I suspect that the problem described in that page - one I have encountered myself in building "Web apps", and which also led me to reject JavaServerPages as a viable solution, as opposed to plain ol' objects - is largely orthogonal to the StaticTyping vs DynamicTyping debate. I would view it more as a matter of JavaServerPages solving the wrong problem, namely, making Web apps look superficially like HTML files. I'm fairly sure that an equivalent framework can be coded in SmallTalk, or Lisp, and reap similar benefits. -- LaurentBossavit
Read my "The Problem" rant before coming to that conclusion. It's not orthogonal at all. -- BradCox
This article doesn't seem to be available at that page. Could you provide a direct link? -- DanielBonniot
I'm not dead sure that I read the right one, but try http://virtualschool.edu/jwaa/ -- cwillu (lurking)
It seems to be partly a factor of modularity's desire for each module to be more or less self-protecting. In strong-typing usually each module will recheck the variable's type as it passes in and out among various modules. This does seem to be redundant. OnceAndOnlyOnce would dictate checking type in as few places as possible. It's somewhat analogous to having your baggage (and ass) checked before you enter the boarding area (or waiting area) in an airport, and once again just before you get on the plane.
For example, if we read a value from an input screen, once its validated that its an "integer", it shouldn't need to be validated again when converted into SQL code for the "save" step. However, most strong-typed systems/philosophies will indeed do it again. Thus, "integer" is stated over and over in many spots. Its perhaps a down-side of modularization. (Somewhat related: OnceAndOnlyOnceCommunismDiscussion).
--top
"In strong-typing usually each module will recheck the variable's type as it passes in and out among various modules. This does seem to be redundant."
Strong-typing is intended to guarantee (at least) type-safety, so type checks are only "redundant" insofar as the language model (compiled vs interpreted) or type model (static vs dynamc) or optimisation mechanisms (or lack thereof) must perform type checks in order to guarantee type-safety. A given compiled language with static typing might only perform type-checks at compile-time, with none done at run-time. An interpreted language with dynamic typing might have to execute type checks in every expression evaluation, assignment, function call, etc.