From: Trevor D. (Twylite) <tw...@cr...> - 2013-03-03 15:34:58
|
Hi, I have recently had the need to use msgcat from within a TclOO object, and found the interaction to be somewhat awkward. I put a proposed solution on http://wiki.tcl.tk/1488 (discussion "Using msgcat with TclOO"), which Harald Oehlmann has asked me to turn into a TIP. Before I do that I'd like to get some feedback from other TclOO users on the preferred behaviour of a TclOO/msgcat integration. Background: I have a TclOO mixin "Configurable" in a namespace "::app", i.e. the class ::app::Configurable. That class declares a method "cget" that takes one argument "optName", and throws an error "unknown option '$optName'" if the named option is not defined for the instance. I would like to use msgcat to provide regional translations of the error, i.e. error [::msgcat::mc "unknown option '%s'" $optName]. ::msgcat::mc works with namespaces, and will look for translations in the current namespace, then the parent namespace, and so on up to the global namespace. Ideally I would like to define a message associated with ::app::Configurable, but that command is in the namespace ::app, represents an object in the dynamically created namespace (e.g.) ::oo::Obj11, and I am executing ::msgcat::mc from within an object instance running in another dynamically created namespace (e.g.) ::oo::Obj13. A call to msgcat::mc will attempt to find translations in ::oo::Obj13, then ::oo, then ::. In my particular case there is one .msg file (per locale) to localise the entire application, rather than one .msg file (per locale) per package. This implies that messages should be defined in a static namespace associated with the name of the class, rather than in a namespace dynamically allocated to the class. I am not unduly attached to this approach. Code: namespace eval ::app { oo::class create Configurable { method cget {optName} { foreach cmd {"namespace current" "self" "self namespace" "self class"} { puts "$cmd = [{*}$cmd]" } puts "info object namespace [self class] = [info object namespace [self class]]" error [::msgcat::mc "unknown option '%s'" $optName] } } } oo::class create Dog { mixin ::app::Configurable } Dog create d d cget bar -> namespace current = ::oo::Obj13 -> self = ::d -> self namespace = ::oo::Obj13 -> self class = ::app::Configurable -> info object namespace ::app::Configurable = ::oo::Obj11 -> unknown option 'bar' Proposed solution (http://wiki.tcl.tk/1488): Put messages for a class ::$ns::$cls into the namespace ::msgs::$ns::$cls, and provide helpers 'oo::define $cls mcset' and '::msgcat::classmc' to respectively set and retrieve catalog messages. # When [oo::define $class mcset $arg1 $arg2 ...] is called on # $class = ::app::Configurable, call instead # [::msgcat::mcset $arg1 $arg2 ...] in the namespace ::msgs::app::Configurable proc ::oo::define::mcset {args} { # See http://wiki.tcl.tk/21595 for identifying the current class under definition set cls [uplevel 2 [list namespace which [lindex [info level -1] 1]]] namespace eval ::msgs${cls} [list ::msgcat::mcset {*}$args] } # When [::msgcat::classmc $arg1 $arg2 ...] is called in a method 'cget' # defined on class/mixin '::app::Configurable', call instead # [::msgcat::mc $arg1 $arg2 ...] in the namespace ::msgs::app::Configurable proc ::msgcat::classmc {args} { namespace eval ::msgs[uplevel 1 {self class}] [list [namespace current]::mc {*}$args] } Translations in .msg files can call ::msgcat::mcset in the namespace ::msgs::app::Configurable. The result is that ::msgcat::classmc is a Do-What-I-Mean for looking up messages from inside an object/instance. oo::class create ::app::Configurable { mcset en "unknown option '%s'" method cget {optName} { error [::msgcat::classmc "unknown option '%s'" $optName] } } Problems: This solution doesn't allow for message sharing or inheritance like msgcat::mc does with respect to namespaces: If I have a class ::app::Beta that is a subclass of ::util::Alpha, and I have a message "foo" defined for Alpha but not for Beta, and I look up "foo" from within a method declared on Beta, should that resolve to Alpha's "foo" message, or to a "foo" message in the namespace ::app in which class ::Beta is declared, or to something else entirely? I'm not sure of the best answer here, and I think it should be resolved before formalising this behaviour in a TIP. The proposed solution doesn't provide any reasonable form of message inheritance. An OO style inheritance that searches superclasses and mixins in method dispatch order would require a new TclOO helper. Message inheritance that uses the class's namespace would involve putting messages into ::$ns::$cls rather than ::msgs::$ns::$cls, which may be more likely to interfere with developer's code or to promote confusion between a class name, the class object's namespace, and the message namespace with the same name as the class name. There is no good reason that messages cannot be defined on any object; the proposed solution is limited to classes. I frequently use a named object as an alternative to a namespace ensemble, and can imagine the need for message catalogs in this context. Would there ever be a reason to override a class message at instance level (similar to overriding a class-defined method using oo::objdefine)? There also may be other problems with the proposed solution that I have not yet noticed. Feedback/comments welcomed. Regards, Twylite |
From: Donal K. F. <don...@ma...> - 2013-03-04 11:02:04
Attachments:
donal_k_fellows.vcf
|
Tl;dr summary: I support the general ideas, if not the detail, and I am willing to write code to make things better. ;-) On 03/03/2013 14:50, Trevor Davel (Twylite) wrote: > I have recently had the need to use msgcat from within a TclOO object, > and found the interaction to be somewhat awkward. I put a proposed > solution on http://wiki.tcl.tk/1488 (discussion "Using msgcat with > TclOO"), which Harald Oehlmann has asked me to turn into a TIP. Before > I do that I'd like to get some feedback from other TclOO users on the > preferred behaviour of a TclOO/msgcat integration. I think the integration between the two models — msgcat assumes that packages are namespaces, TclOO turns namespaces into instances — needs to be better. The models don't mix well at all right now! However, we probably need to think in terms of how to define what we want the interface to look like, rather than what the minimum amount of hacking to make it work is. 1. We want to be able to associate message catalogs with classes, preferably at "declaration" time. 2. We want lookup from within an object to be "simple". 3. We want inheritance of catalogs. 4. We want to be able to associate catalogs with instances too. 5. We want lookup to be associated with the current class structure; no precompilation of the catalog mapping. I'm probably missing things. I have no idea if the class ::foo::Bar should use the ::foo namespace for catalogs. > The proposed solution doesn't provide any reasonable form of message > inheritance. An OO style inheritance that searches superclasses and > mixins in method dispatch order would require a new TclOO helper. > Message inheritance that uses the class's namespace would involve > putting messages into ::$ns::$cls rather than ::msgs::$ns::$cls, which > may be more likely to interfere with developer's code or to promote > confusion between a class name, the class object's namespace, and the > message namespace with the same name as the class name. > > There is no good reason that messages cannot be defined on any object; > the proposed solution is limited to classes. I frequently use a named > object as an alternative to a namespace ensemble, and can imagine the > need for message catalogs in this context. Would there ever be a reason > to override a class message at instance level (similar to overriding a > class-defined method using oo::objdefine)? I would be quite open to extending the code of TclOO to add any critical support features required (e.g., getting a flattened inheritance hierarchy) but it is my instinct that catalogs should be associated with classes and not their containing namespaces. That makes more sense with instances though (as many objects are under the ::oo namespace, and that's never going to have messages for the application). Let's decide what the interface should be from a user's perspective, and make it work like that. :-) Donal. |
From: Trevor D. (Twylite) <tw...@cr...> - 2013-03-04 11:42:32
|
Hi, On 2013/03/04 01:01 PM, Donal K. Fellows wrote: > 1. We want to be able to associate message catalogs with classes, > preferably at "declaration" time. > 2. We want lookup from within an object to be "simple". > 4. We want to be able to associate catalogs with instances too. Along the lines of what I've proposed already, I think the following will be sufficient for 1, 2 & 4: (a) [oo::define $class mcset] and [oo::objdefine $obj mcset] to define messages on classes & object, handling both "declaration" time and dynamic declarations. (b) Messages are stored in predictable namespaces based on the class/object name. This allows .msg files containing localisations to be loaded before or after the class/object is created. It also makes the "define/objdefine mcset" functions in (a) rather simple helpers that identify the class/object being defined and execute msgcat::mcset in the appropriate namespace. (c) For "simple" lookup there seem to be three options: (c.1) A function like msgcat::mc, but one that uses object-based lookup rules rather than namespace-based rules. I have called this "classmc" for now, but I'm sure a better name can be found. (c.2) A method "my mc". (c.3) A class method "[self class] mc". This is uglier to call, but allows the caller to be specific about which message to use (overriding hierarchy navigation). Then the question is: which option? Some thoughts: - I think it is essential that msgcat::mc and msgcat::classmc (or equivalent) are distinct; msgcat::mc must not determine that it is being called from an object and attempt a different behaviour (that would be confusing and would be a compatibility break). - I think that good design demands that this functionality is provided by msgcat (using TclOO enhancements where necessary), and not by TclOO. For (c.2) and (c.3) that implies that defining a message on a class must implicitly add a mixin to the class (or class object, or object) that provides the "mc" method. - (c.3) makes it possible to access messages from outside an instance, which is not possible with (c.2). You could do it with a variant of (c.1) that allows you to specify where to start looking (i.e. class/object), but the API for that variant may be ugly. I'm inclined towards the msgcat::classmc (c.1) approach, but I don't have a strong reason for that. > 3. We want inheritance of catalogs. > 5. We want lookup to be associated with the current class structure; no > precompilation of the catalog mapping. > > I have no idea if the class ::foo::Bar should use the ::foo namespace > for catalogs. Given (5), I would say that if lookup is associated with the current class structure, then inheritance of catalogs should match OO inheritance, and namespaces should have nothing to do with message lookup. Otherwise it becomes too complex to reason about catalog inheritance. If you have a catalog of messages for classes in the ::foo namespace, then you could create a mixin ::foo::Messages. > I would be quite open to extending the code of TclOO to add any critical > support features required (e.g., getting a flattened inheritance > hierarchy) I think this is a critical feature for TclOO, even if it turns out not to be required in this case. An alternative construction (possibly more useful) would be an lmap-like function that "visits" each object/class in method dispatch order and evaluates a code block in the private namespace, respecting break/continue. > but it is my instinct that catalogs should be associated with > classes and not their containing namespaces. Agreed. > Let's decide what the interface should be from a user's perspective, and > make it work like that. :-) Also agreed; that's mostly what I want to discuss here. Regards, Twylite |
From: Trevor D. (Twylite) <tw...@cr...> - 2013-03-04 15:55:55
|
Hi, On 2013/03/04 01:42 PM, Trevor Davel (Twylite) wrote: > (c.1) A function like msgcat::mc, but one that uses object-based lookup > rules rather than namespace-based rules. I have called this "classmc" > for now, but I'm sure a better name can be found. > (c.2) A method "my mc". > (c.3) A class method "[self class] mc". This is uglier to call, but > allows the caller to be specific about which message to use (overriding > hierarchy navigation). I've done a bit more thinking and some digging into the msgcat code, and I think I've found an API that I like: # Working with classes and objects oo::class create ::Foo { mcset en Msg1 "This is the first message" } oo::class create ::Bar { mcset en Msg2 "This is the first message" method test {} { puts "[mymc Msg1]; [mymc Msg2]" } } oo::define ::Bar mcset en Msg2 "Fixed definition of the second message" ::Bar create mybar oo::objdefine mybar mcset en Msg1 "Object-specific first message override" # A non-object needs to reuse a class message msgcat::oo::mc ::Bar Msg2 # Translations in a .msg file msgcat::oo::mcset ::Foo af Msg1 "Die eerste boodskap" Comments: - oo::define::mcset and oo::objdefine::mcset are thin wrappers around msgcat::oo::mcset - oo::Helpers::mymc is a thin wrapper around msgcat::oo::mc - msgcat doesn't actually store information in namespaces; it uses a multi-level dict that uses namespace as a key at one level. msgcat::oo::mcset does the same job as msgcat::mcset, but in a dict with object names as a key at one level. msgcat::oo::mc does the same job as msgcat::mc, but uses the oo dict and searches the object hierarchy (in method dispatch order) rather than parent namespaces. - I prefer "mymc" to "my mc" as the latter is strictly read as "use the message dispatch algorithm to locate and execute a method 'mc' defined on or inherited by this object" where mc will "locate and return a named message associated with object, for the current locale, by searching for message definitions on the object and its inheritance tree in message dispatch order", and requires the mc method to live somewhere. By comparison "mymc" is to messages as "my" is to methods, and doesn't pollute the object's method list. - The implementation requires a TclOO extension oo::InfoObject::dispatchOrder that returns a flattened inheritance hierarchy (including mixins) in method dispatch order. Code is below. In trying to write a Tcl version of [info object dispatchOrder] that follows the Method Dispatch Algorithm in http://www.tcl.tk/cgi-bin/tct/tip/257.html, I found that the actual dispatch order seems different: oo::define oo::class method m {} { puts [self class]; next } oo::define oo::object method m {} { puts [self class]; next } oo::class create SuperMix { method m {} { puts [self class]; next } } oo::class create Mix { superclass SuperMix ; method m {} { puts [self class]; next } } oo::class create Super { method m {} { puts [self class]; next } } oo::class create OtherSuper { mixin Mix ; method m {} { puts [self class]; next } } oo::class create MyClass { superclass Super OtherSuper; method m {} { puts [self class]; next } } MyClass create myclass myclass m -> ::myclass -> ::MyClass -> ::Super -> ::Mix -> ::SuperMix -> ::OtherSuper -> ::oo::object -> no next method implementation There being no filters at all, or mixins on myclass, I would have expected the method declared by the mixin Mix on the superclass OtherSuper to have taken precedence according to TIP #257 Algorithm rule 5. This could well be my misunderstanding; or are the TclOO dispatch rules different from the algorithm in TIP #257? Here's the proposed msgcat code: # http://wiki.tcl.tk/21595 proc ::oo::DefWhat {} { uplevel 3 [list namespace which [lindex [info level -2] 1]] } proc ::oo::define::mcset {args} { ::msgcat::oo::mcset [::oo::DefWhat] {*}$args } proc ::oo::objdefine::mcset {args} { ::msgcat::oo::mcset [::oo::DefWhat] {*}$args } proc oo::Helpers::mymc {args} { tailcall ::msgcat::oo::mc [uplevel 1 self] {*}$args } namespace eval ::msgcat::oo { set Msgs {} } proc ::msgcat::oo::mcset {obj locale src dest} { variable Msgs if {[llength [info level 0]] == 4} { ;# dest not specified set dest $src } set locale [string tolower $locale] # create nested dictionaries if they do not exist if {![dict exists $Msgs $locale]} { dict set Msgs $locale [dict create] } if {![dict exists $Msgs $locale $obj]} { dict set Msgs $locale $obj [dict create] } dict set Msgs $locale $obj $src $dest return $dest } proc ::msgcat::oo::mc {obj src args} { # Check for the src in the object hierarchy of obj variable Msgs variable ::msgcat::Loclist foreach {obj objNs} [info object dispatchOrder $obj] { foreach loc $Loclist { if { [dict exists $Msgs $loc $obj $src] } { if {[llength $args] == 0} { return [dict get $Msgs $loc $ns $src] } else { return [format [dict get $Msgs $loc $ns $src] {*}$args] } } } } # else fall back to mcunknown as in msgcat::mc ? } Regards, Twylite |