Author: Harald Oehlmann <oehhar@sourceforge.com>
State: Voting
Type: Project
Vote: In progress
Created: 07-Dec-2017
Post-History:
Keywords: msgcat, oo
Tcl-Version: 8.7
Abstract
Package msgcat implements message catalogues for packages organized in nested namespaces. This TIP proposes the extension to TclOO.
Rationale
Since TclOO was included in the core, packages may also be defined as TclOO classes or classless objects. The msgcat package should feature this.
A package should have its methods within a package namespace:
namespace eval ::foo { oo::class create Foo } package provide foo 1.0
The message catlog belongs to the package, not to an individual class.
namespace eval ::foo { msgcat::mcload $dir/msgs oo::class create Foo { ... } } package provide foo 1.0
Documentation
There are 4 use-cases to consider (which may be intermixed in the same package):
(with 'Servus!' as translation for 'Hi!')
Use-Case 1: the package does not use OO
namespace import msgcat::* namespace eval ::N1 { mcload $dir/msgs proc m1 {} { puts [mc Hi!] } } % N1::m1 -> Servus!
Use-case 2: msgcat is called within a class definition script
% namespace import msgcat::* % namespace eval ::N2 { mcload $dir/msgs oo::class create C1 {puts [mc Hi!]} } -> Servus!
Use-case 3: msgcat is called from a method in an object and the method is defined in a class
namespace import msgcat::* namespace eval ::N3Class { mcload $dir/msgs oo::class create C1 oo::define C1 method m1 { puts [mc Hi!] } } # The class object may be used in another namespace namespace eval ::N3Obj { set O1 [::N3Class::C1 new] } % $N3Obj::O1 m1 -> Servus!
Use-case 4: msgcat is called from a method of a classless object
namespace import msgcat::* namespace eval ::N4 { mcload $dir/msgs oo::object create O1 oo::objdefine O1 method m1 {} { puts [mc Hi!] } } % N4::O1 m1 -> Servus!
Use-case 1 may be emulated in Use-case 2 to 4 using "namespace eval"
Before this extension, a programmer for use-case 2 to 4 must have used "namespace eval" to explicitly specify the package namespace:
namespace import msgcat::* namespace eval ::N4 { mcload $dir/msgs oo::object create O1 oo::objdefine O1 method m1 {} { puts [namespace eval ::N4 {mc Hi!}] } } % N4::O1 m1 -> Servus!
This should still work with the new extension for compatibility reasons.
Usage
There are 4 proposed extensions to current msgcat 1.6.1:
Extension 1: Extend all msgcat commands to support all 4 use-cases.
So any msgcat command will detect the scenario on its own and extract the package namespace automatically.
The commands which are packet-namespace related are: mc, mcexists, mcpackagelocale, mcforgetpackage, mcpackagenamespaceget (new command, see below), mcpackageconfig, mcset and mcmset.
This has the following advantages (compared to the alternatives):
- no new commands, no learning
- if another foreign procedure is called and the procedure wants to use the callers message catalog, it may just use "uplevel 1 {msgcat tag}" and does not need to know if it is a class or not.
Here is an example for the second advantage:
The tklib package "tooltip" may invoke msgcat::mc msg
for all textes to get eventual translations (it does something like that but I don't understand it, IMHO broken).
The package namespace of the caller should be used (not the one of the tooltip package).
So:
proc ::tooltip::tooltip {widget message} { ... set message [uplevel 1 {::msgcat::mc $message}] }
This will work in all use-cases, e.g. if tooltip::tooltip
is called by a method following use-case 1 to 4.
Extension 2: new command to get package namespace
The "magic" to extract the package namespace is exposed by the command:
mcpackagenamespaceget
This may be used:
- for the same case like the upper tooltip example, but for late binding
- for introspection and debugging
The upper tooltip example, where the translation is extracted when the tooltip is actually shown (to show an updated message if the current locale changed)
proc ::tooltip::tooltip {widget message} { ... set messagenamespace [uplevel 1 {::msgcat::mcpackagenamespaceget}] ... bind $widget[list ::tooltip::show $widget $messagenamespace $message] } proc ::tooltip::show {widget messagenamespace message} { ... set message [namespace eval $messagenamespace [list ::msgcat::mc $message]] ... }
Examples with the 4 use-cases:
Use-case 1: the package does not use OO
namespace eval ::N1 { proc m1 {} {msgcat::mcpackagenamespaceget} } % N1::m1 -> ::N1
Use-case 2: call within class definition script
% namespace eval ::N2Class { oo::class create C1 {puts "pns=[msgcat::mcpackagenamespaceget]"} } -> pns=::N2Class -> ::N2Class::C1
Use-case 3: The package implementation is done by a class
namespace eval ::N3Class { oo::class create C1 oo::define C1 method m1 {msgcat::mcpackagenamespaceget} } # The class object may be used in another namespace namespace eval ::N3Obj { set O1 [::N3Class::C1 new] } % $N3Obj::O1 m1 -> ::N3Class
Use-case 4: The package implementation is done by a classless object
namespace eval ::N4 { oo::object create O1 oo::objdefine O1 method m1 {} {msgcat::mcpackagenamespaceget} } % N4::O1 m1 -> ::N4
Manually overwrite the package namespace by namespace eval
As stated in the introduction, current oo code might have used an explicit namespace eval packagenamespace
to make msgcat work. This should still be supported:
namespace eval ::N4 { oo::object create O1 oo::objdefine O1 method m1 {} { namespace eval ::N5 { msgcat::mcpackagenamespaceget} } } % N4::O1 m1 -> ::N5
Remark, that in most real cases, one will explicitly specify "::N4" instead "::N5", as this is the package namespace. "::N5" was chosen in the example to show the fact, that the "namespace eval" overwrites any class-magic.
Extension 3: new command to get a translation with a package namespace as argument
A new command is proposed to get a translation with an explicit namespace:
mcn ns src args...with the arguments: * ns: package namespace to do the translation for * src: the translation source string (like mc command) * args: eventual arguments to contained format patterns (like mc command)
This command is identical to the mc
command, with the difference, that the package namespace is not found by an implicit call to mcpackagenamespaceget
, but may be explicitly specified as first argument
Then, the mc command may be expressed like:
mcn [mcpackagenamespaceget] src args...
There are the following purposes for this command:
* foreign packages. The package namespace is known (for example by a call to mcpackagenamespaceget
). The translation may be retrieved by a call to mcn
without any namespace eval $ns
around it.
* Authors of C packages required to specify the namespace explicitly.
* Optimizations in an eventual time critical path. The speed of the old msgcat is beaten by mcn [namespace current]..
An example for the case of a foreign package is the tooltip package described above.
The contained call:
proc ::tooltip::show {widget messagenamespace message} { ... set message [namespace eval $messagenamespace [list ::msgcat::mc $message]] }
may be expressed like:
proc ::tooltip::show {widget messagenamespace message} { ... set message [::msgcat::mcn $messagenamespace $message] }
Extension 4: Command "mcexists" should get a parameter -namespace to explicitly specify the namespace
The command mcexists
has currently the syntax:
mcexists ?-exactnamespace? ?-exactlocale? src
A switch ?-namespace ns? is added to specify the namespace explicitly:
mcexists ?-exactnamespace? ?-exactlocale? ?-namespace ns? src
This may be useful in similar situations as the mcn
command.
Implementation
Within msgcat, the package namespace is currently extracted by:
proc msgcat::mc {src args} { ... set ns [uplevel 1 {namespace current}]
This is replaced by:
proc msgcat::mc {src args} { ... set ns [PackageNamespaceGet] ... } proc ::msgcat::PackageNamespaceGet {} { uplevel 2 { # Check for no object switch -exact -- [namespace which self] { {::oo::define::self} { # We are within a class definition return [namespace qualifiers [self]] } {::oo::Helpers::self} { # We are within an object set Class [info object class [self]] # Check for classless defined object if {$Class eq {::oo::object}} { return [namespace qualifiers [self]] } # Class defined object return [namespace qualifiers $Class] } default { # Not in object environment return [namespace current] } } } }
The implementation is in tcl fossil in branch tip490-msgcat-oo-2.
There are tests but no man page changes jet. Please use this text as man-page.
Discussion
Issue 1: are really all oo use-cases covered ?
Gosts like multiple inheritance, mixins, filters whatever are calling...
Issue 2: All backward compatibility issues covered ?
I see the following covered ones:
- no oo ->
uplevel 1 namespace current
is used, o.k. - oo with explicit
namespace eval
-> o.k.
Are there eventually other cases not discussed here?
Issue 3: performance loss
There is a performance penalty due to the modification described in "implementation".
To not decrease performance would require to introduce new commands, what I think is not good. We would loose the flexibility and backward compatibility to just insert a 3rd party package, which may call "uplevel 1 msgcat::mc tag", and it works.
For performance enthusiasts, one may use "mcn", which may be little faste than former msgcat.
Issue 4: does not work for tcl 8.6
The self
command does not work for tcl 8.6 within class definition scripts.
It returns the error message:
Alternatives
Use a mixin
Proposed by Donal Fellows
Each class which wants to use message catalogs require a call to mixin msgcat::MessageCatalogAware
.
Then, the following commands are available and correspond to the msgcat::
commands:
- my mc
- my mcmax
- my mcexists
All other mc-commands are not available within classes and should be used outside of a class.
Examples
Within the following example, the message catalog for the package is loaded outside the method definitions.
A class "Foo" is created and the msgcat capabilities are activated by the mixin.
Then, a method is defined, which uses the message catalog of the namespace.
namespace eval ::foo { msgcat::mcload $dir/msgs oo::class create Foo { mixin msgcat::MessageCatalogAware method printMessage {x y} { puts [my mc "FooPrintMessage(%d,%d)" $x $y] } } } package provide foo 1.0
The implementation is in tcl fossil in branch tip490-msgcat-oo.
Use own commands for getting and setting message catalogues
Eric Boudallier proposed a solution on Wiki page "msgcat and TclOO"
There is also an addendum by Donal Fellows with an alternate implementation.
mixin solution where superclass and subclass may not be in different namespaces:
Donal Fellows also provided this solution.
The key then is how to make msgcat::mc
work (and a few related commands as well). The trick I've thought of is to use the magic powers of apply
and tailcall
; here's what we're looking for:
namespace eval ::foo { msgcat::mcload $dir/msgs oo::class create MessageCatalogAware { forward mc apply {args {tailcall ::msgcat::mc {*}$args} ::foo} unexport mc } oo::class create Foo { mixin MessageCatalogAware # ... } oo::class create Bar { mixin MessageCatalogAware # ... } } package provide foo 1.0
(I'd use a mixin for this; it's not really about the class itself; it's just about making a capability available.)
It'd be nice to have a way to make that bridging class automatically. Here it is.
oo::class create ::msgcat::oobridge { superclass oo::class self method create {name args} { set cls [next $name {*}$args] set ns ::[namespace qualifiers [namespace which $cls]] foreach cmd {mc mcmax mcexists} { oo::define $cls forward $cmd apply [list \ {cmd args} {tailcall $cmd {*}$args} $ns] \ ::msgcat::$cmd oo::define $cls unexport $cmd } return $cls } }
Now our little package would become:
namespace eval ::foo { msgcat::mcload $dir/msgs msgcat::oobridge create MessageCatalogAware oo::class create Foo { mixin MessageCatalogAware # ... } oo::class create Bar { mixin MessageCatalogAware # ... } } package provide foo 1.0
Alternatively, we could plug the capabilities into oo::define
; the principles of the code are largely the same but the details are little different.
I've been thinking about this more, and I've realised that there is a subtle problem: a superclass and a subclass could be in different namespaces in different packages, and so could reasonably want to use different message catalogs. This means that the bridging code always needs to look up what the context namespace is at runtime, which in turn means that there's no need for clever class manufacturing, and it can all be done with (see section "Implementation" above).
Credits
- René Zaumseil: initiative and partial implementation
- Eric Boudallier: alternate implementation
- Donal Fellows: implementation and examples
- Ashok P. Nadkarni: teach me oo by his excellent book
Copyright
This document has been placed in the public domain.