TIP 490: msgcat for tcloo

Login
Bounty program for improvements to Tcl and certain Tcl packages.
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:

wrong # args: should be "self arg ?arg ...?"

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.

History