Difference between revisions of "Enums in Eiffel"
m (→An Example) |
m (→Using Pseudo Enum Types) |
||
Line 533: | Line 533: | ||
do | do | ||
inspect style.item | inspect style.item | ||
− | when {BORDER_STYLE}.none | + | when {BORDER_STYLE}.none then |
... | ... | ||
− | when {BORDER_STYLE}.flat | + | when {BORDER_STYLE}.flat then |
... | ... | ||
− | when {BORDER_STYLE}.rounded | + | when {BORDER_STYLE}.rounded then |
... | ... | ||
− | when {BORDER_STYLE}.embossed | + | when {BORDER_STYLE}.embossed then |
... | ... | ||
else | else |
Revision as of 14:45, 7 May 2007
Contents
Preface
Something pondered for years was the question regarding why Eiffel has never embraced Enum types or a variant more in style with the Eiffel paradigm. There are a number of comments from numerous developers regarding why Enums are "Bad". Most languages exhibit bad ideals and some more than others. The congisen is that if you give a developer a tool to abuse it will be abused and sometimes by the seasoned developers. Generally, seasoned developers have a grasp of the dangers of abusing aspects of a language to gain performance or micro design. However lessons are learned from those who know better from those that know less. As such bad programming practices creep down the chain until it become a common convention.
In this document I'll outline the pontetial dangers and the oddities found commonly with the "Enum" type and attempt to dispell them with a solution to implementing Enums in Eiffel. First and foremost, Why does Eiffel need an Enum type...
Why Does Eiffel Need an Enum Type
The reasons are numerous and leaves us asking why they were not approached in the ECMA specification.
As Eiffel evolves and rears its head into the mainstream, its application domain expands. Users, libraries and complexity all grow as a languages does. It has been long said that Eiffel is almost unique in its ability to self-document classes and routines through terse comments and contracts. However, Eiffel for being so terse with commenting, is extremely verbose with class interfaces which can be trying at times.
Example Listing
To demonstrate a number of point here, a real example is going to be used. The example below is code taken from EiffelVision2:
deferred class EV_TEXT_ALIGNABLE feature -- Access text_alignment: INTEGER is -- Current alignment. -- See class EV_TEXT_ALIGNABLE_CONSTANTS for -- possible values. require not_destroyed: not is_destroyed ensure bridge_ok: Result = implementation.text_alignment feature -- Status report is_left_aligned: BOOLEAN is -- Is `Current' left aligned? require not_destroyed: not is_destroyed is_center_aligned: BOOLEAN is -- Is `Current' center aligned? require not_destroyed: not is_destroyed is_right_aligned: BOOLEAN is -- Is `Current' right aligned? require not_destroyed: not is_destroyed feature -- Status setting align_text_center is -- Display `text' centered. require not_destroyed: not is_destroyed ensure alignment_set: is_center_aligned align_text_right is -- Display `text' right aligned. require not_destroyed: not is_destroyed ensure alignment_set: is_right_aligned align_text_left is -- Display `text' left aligned. require not_destroyed: not is_destroyed ensure alignment_set: is_left_aligned invariant valid_alignment: (create {EV_TEXT_ALIGNMENT_CONSTANTS}).valid_alignment (text_alignment) end
Enums for Brevity
Eiffel can be quite verbose when it comes to its class' interfaces. EV_TEXT_ALIGNABLE
demonstates this very well. EV_TEXT_ALIGNABLE
contains seven features; three used to query an "alignable" state, three to set it and the final text_alignment
attribute, used to store a state code according to a constant definition defined elsewhere.
The alternative to the verbose Status report and Status setting features is to use a class attribute that represents an object's mode, state or another binary attribute. Flags can also be defined in this fashion where bit operations are used to extrapolate meaning. This does not lend itself well to a well designed interface. It is much easier for a developer to conceptualize design using words instead of a numerical constant and bit operations, hence such verboseness in the Eiffel exported interfaces today.
Self Documenting
EV_TEXT_ALIGNABLE.text_alignment
also shows yet another problem with using INTEGER
or another such numerical type as a form of state representation - it's meaningless! Eiffel's ability to assist developers in self documentation is not apparent here. The only way to realized the underlying meaning of text_alignment
is to read the comments, which indicates where to look for the associated constant values associated with an EV_TEXT_ALIGNABLE
object's state.
For documentation, there is the class invariant check found in EV_TEXT_ALIGNABLE_CONSTANTS.valid_alignment
but only those who wish to descend the implementation EV_TEXT_ALIGNABLE
are bothered about the invariant as it only applies to the state validation of an EV_TEXT_ALIGNABLE
object and not to a client. In addition valid_alignment
could potentially return True
for an non-respected value (more on this later.)
Safety
One of the major disadvantages of not having Enums is the lack of safety. Safety encompassed both type safety and future proofing.
In EV_TEXT_ALIGNMENT
there is already text_alignment
, which exhibits the inherent problem with type safety through the lack of an Enum type (discussed next.) No only is a flag attribute present but there are the status setting routines align_text_left
, align_text_right
and align_text_center
. On top of that, for the sake of code clarity for clients there are the status queries is_left_aligned
, is_right_aligned
and is_center_aligned
. The status setting and query routines hide the implementation details of having to know and use EV_TEXT_ALIGNMENT_CONSTANTS
, which is a good thing but can also be very frustrating when writing effective code using this interface.
To demonstrate, assume a graphical editor has been developed using EiffelVision2. In the editor the user selects a region of text which should enable tool bar buttons used to manipulate the alignment of the selected region of text.
on_text_selected require has_selection: has_selection local l_alignable: EV_TEXT_ALIGNABLE do l_alignable ?= selected_entity if l_alignable /= Void then if l_alignable.is_left_aligned then active_button := left_aligned_button elseif l_alignable.is_center_aligned then active_button := center_aligned_button elseif l_alignable.is_right_aligned then active_button := right_aligned_button else -- New alignment not respect! check False end end end if active_button /= Void alignment_button_group.set_active_button (active_button) alignment_button_group.enable_sensitive else alignment_button_group.disable_sensitive end end
A Quick Enum Conversion
Using an Enum type for the exemplary EV_TEXT_ALIGNMENT
, the code is reduced significatly:
deferred class EV_TEXT_ALIGNABLE feature -- Access text_alignment: EV_TEXT_ALIGNMENT assign set_text_alignment -- Current alignment. require not_destroyed: not is_destroyed feature -- Status setting set_text_alignment (alignment: like text_alignment) -- Set `text_alignment' to `alignment' require not_destroyed: not is_destroyed ensure alignment_set: text_alignment = alignment end
At first look out of seven features it has been reduced to only two. That alone is a major change in the amount of code that has to be written for a library author. The EV_TEXT_ALIGNABLE
is actually deferred so that is also a lot less code an implementor has to write. It does not stop there, with the type safety introduced, by changing text_alignment
to use a Enum type instead of an integer, the class invariant has been removed as have the reference comments needed in text_alignment
to explain exactly which class needs to be used, containing the constants, to use text_alignment
correctly.
An Example
To be more concrete regarding the type-safety, code brevity and safety in extending styles new Enum types could bring, here is an example. The first example is written for Eiffel as it stands now. The example is from a fictitious graphical tool used to modify the text alignment of any EV_TEXT_ALIGNABLE
descended widget hosted as a child of a EiffelVision2 window. The important point about the example is that it handles creation, event hookup and event handling automatically.
feature {NONE} -- Initialization create_alignment_buttons -- Create alignment group tool bar buttons do add_alignment_button (once "left", {EV_TEXT_ALIGNMENT_CONSTANTS}.left) add_alignment_button (once "center", {EV_TEXT_ALIGNMENT_CONSTANTS}.center) add_alignment_button (once "right", {EV_TEXT_ALIGNMENT_CONSTANTS}.right) end add_alignment_button (a_name: STRING; a_alignment: INTEGER) is -- Creates an adds an alignment tool bar button require a_name_attached: a_name /= Void not_a_name_is_empty: not a_name.is_empty valid_alignment: (create {EV_TEXT_ALIGNMENT_CONSTANTS}).valid_alignment ( a_alignment) local button: EV_BUTTON do create button.make button.set_pixmap (pixmap_loader (once "button_" + a_name)) button.click_action.extend (agent on_alignment_button_clicked (a_alignment)) alignment_group.extend (button) end feature {NONE} -- Event handlers on_alignment_button_clicked (a_alignment: INTEGER) is -- Called when an alignment tool bar button is selected require valid_alignment: (create {EV_TEXT_ALIGNMENT_CONSTANTS}).valid_alignment ( a_alignment) local l_alignable: EV_TEXT_ALIGNABLE do l_alignable ?= selected_widget if l_alignable /= Void then inspect a_alignment when {EV_TEXT_ALIGNMENT_CONSTANTS}.left then l_alignable.align_text_left when {EV_TEXT_ALIGNMENT_CONSTANTS}.center then l_alignable.align_text_center when {EV_TEXT_ALIGNMENT_CONSTANTS}.right then l_alignable.align_text_right -- No else because if a new alignment is -- added we want an exception to be raised. end end end
For a comparison in brevity here is the code using an Enum type:
feature {NONE} -- Initialization create_alignment_buttons -- Create alignment group tool bar buttons. do (create {EV_TEXT_ALIGNMENT}).items.do_all (agent (a_item: EV_TEXT_ALIGNMENT) local button: EV_BUTTON do create button.make button.set_pixmap (pixmap_loader ( once "button_" + a_item.out)) button.click_action.extend (agent on_alignment_button_clicked (a_item)) alignment_group.extend (button) end end feature {NONE} -- Event handlers on_alignment_button_clicked (a_alignment: EV_TEXT_ALIGNMENT) -- Called when an alignment tool bar button is selected. local l_alignable: EV_TEXT_ALIGNABLE do l_alignable ?= selected_widget if l_alignable /= Void then l_alignable.text_alignment := a_alignment end end
A feature has been removed and replaced with an inline agent because it can be done using the items
feature of an Enum type. Preconditions have also been removed to check the type of alignment because this will be statically checked by the compiler. Brevity and type safety are great additions but the most important here is the Enum type based code respects any new Enum type added at a later date, which the first non-Enum type base code cannot do!
The widget that is alignable is responsible for the adjustment in the display when an alignment is set on it. In the first example code the alignment is set using a routine that corresponds to a constant in another class. If a new constant is added, and a new alignment type established, the application will not show the tool bar button or be able to respect any new type in the general purpose event handler routine. The application author must be vigilantly aware of the changes made to EV_TEXT_ALIGNMENT
.
The next problem with the first code example is the precondition contract condition EV_TEXT_ALIGNMENT_CONSTANTS.valid_alignment
. The contract will succeed in passing through even through add_alignment_button
and on_alignment_button_clicked
do not support a new type of alignment! This is because the modifier of EV_TEXT_ALIGNMENT_CONSTANTS
will have surely added it to the list of supported alignments. The code is completely broken according to the contract conditions.
In the Enum-type example all these problems disappear. There is no need for the preconditions because of the type safety. The setting of the alignment is delegated directly to the widget itself, so any new style is automatically propagated.
A Temporary Solution
Enum types can be created in Eiffel as it stands today, albeit not perfectly. I've managed to create psuedo Enum types using Eiffel 6.0, but each Enum type requires a little more from an Enum type author than should be needed. In addition the psuedo Enum types are based on basic numeric classes so Enum type members have to be declared as constants of a basic numeric type. This is no ideal but it works.
A Pseudo Enum Type
Here is the code for a psuedo Enum type:
indexing description: "[ Base Enum implementation. ]" deferred class ENUM [G -> NUMERIC] inherit HASHABLE redefine default, default_create, is_equal, out end PART_COMPARABLE redefine default_create, is_equal, out end convert item: {G} feature {ENUM} -- Initialization frozen default_create is -- Default initialization do make ((items[1]).item) end frozen make (n: G) is -- Initializes instance from base entity `n' -- -- `n': A numerical value associated with a member of `Current' require n_is_valid_numeric_value: is_valid_numeric_value (n) do internal_value := n ensure internal_value_set: internal_value = n end feature -- Access frozen items: ARRAY [like Current] -- Access to all members of `Current' local l_items: ARRAY [G] l_instance: like Current l_internal: INTERNAL l_id: INTEGER l_count, i: INTEGER l_assert: BOOLEAN do create l_internal l_id := l_internal.dynamic_type (Current) Result ?= internal_items_table.item (l_id) if Result = Void then check not_internal_items_table_has_l_id: not internal_items_table.has (l_id) end l_items := members l_count := l_items.count create Result.make (1, l_count) l_assert := {ISE_RUNTIME}.check_assert (False) from i := 1 until i > l_count loop -- Does automatic conversion l_instance ?= l_internal.new_instance_of (l_id) l_instance.make (l_items.item (i)) Result.put (l_instance, i) i := i + 1 end l_assert := {ISE_RUNTIME}.check_assert (l_assert) internal_items_table.force (Result, l_id) end ensure result_attached: Result /= Void not_result_is_empty: not Result.is_empty internal_items_table_has_current: internal_items_table.has ((create {INTERNAL}).dynamic_type (Current) ) end frozen hash_code: INTEGER is -- Hash code value local l_hashable: HASHABLE do l_hashable ?= internal_value if l_hashable /= Void then Result := l_hashable.hash_code else check False end end end feature {ENUM} -- Access frozen internal_items_table: HASH_TABLE [ARRAY [ENUM [NUMERIC]], INTEGER] is -- Items table used to cache member info. once create Result.make (1) ensure result_attached: Result /= Void end frozen internal_value: G -- Internal raw value (do not rename!) feature -- Query is_valid_numeric_value (n: G): BOOLEAN is -- Determines if `n' is a value associated with a member of `Current'. -- -- `n': A numerical value to check for validity against members of `Current'. -- `Result': True if `n' is a valid member, False otherwise. local l_assert: BOOLEAN do Result := True l_assert := {ISE_RUNTIME}.check_assert (False) -- Kind of a hack but it's the most direct way to check. -- `n' is converted into `like Current' Result := members.has (n) l_assert := {ISE_RUNTIME}.check_assert (l_assert) end feature {NONE} -- Factory members: ARRAY [G] is -- Array of all members of `Current'. -- -- Note to Implementers: This function should be a once! deferred ensure result_attached: Result /= Void not_result_is_empty: not Result.is_empty result_lower_is_one: Result.lower = 1 result_upper_is_count: Result.upper = Result.count result_contains_unique_items: (agent (a_result: ARRAY [G]): BOOLEAN require a_result_attached: a_result /= Void local l_upper, i: INTEGER do Result := True l_upper := a_result.upper from i := a_result.lower until i > l_upper or not Result loop Result := a_result.occurrences (a_result[i]) = 1 i := i + 1 end end).item ([Result]) same_result: Result = members end feature -- Comparison frozen is_equal (other: like Current): BOOLEAN is -- Is `other' attached to an object considered -- equal to current object? -- -- `other': Other instance to compare against. -- `Result': True if Current is equal to `other', False otherwise. local l_other: ENUM [G] do l_other ?= other if l_other /= Void then Result := internal_value.is_equal (l_other.internal_value) end end frozen infix "<" (other: like Current): BOOLEAN -- Is current object less than `other'? -- -- `other': Other instance to compare against. -- `Result': True if Current is less than `other', False otherwise. local l_other: ENUM [G] l_cc, l_oc: PART_COMPARABLE do l_other ?= other if l_other /= Void then l_cc ?= internal_value l_oc ?= l_other.internal_value if l_cc /= Void and l_oc /= Void then Result := l_cc < l_oc end end end feature -- Conversion frozen item: G is -- `Current' as a {NUMERIC} value do Result := internal_value ensure result_set: Result = internal_value result_is_valid_numeric_value: is_valid_numeric_value (Result) end feature -- Output out: STRING is -- New string containing terse printable representation -- of current object do Result := internal_value.out end invariant is_valid_numeric_value: is_valid_numeric_value (internal_value) end
A Custom Pseudo Enum Type
The following code shows the declaration of a new psuedo Enum type as possible in Eiffel 6.0:
expanded class BORDER_STYLE inherit ENUM [NATURAL] create default_create, make convert make ({NATURAL}), item: {NATURAL} feature -- Access none: NATURAL = 1 flat: NATURAL = 2 rounded: NATURAL = 3 embossed: NATURAL = 4 feature {NONE} -- Factory members: ARRAY [NATURAL] is -- Array of all members of `Current' once Result := <<none, flat, rounded, embossed>> end end
With psuedo Enum types create
declarations, convert
declarations and members
needs to be implemented and populated correctly.
Using Pseudo Enum Types
And now the usage. Because each psuedo Enum type member is declared as a basic numeric type constant they can be accessed as such.
usage (style: BORDER_STYLE) do inspect style.item when {BORDER_STYLE}.none then ... when {BORDER_STYLE}.flat then ... when {BORDER_STYLE}.rounded then ... when {BORDER_STYLE}.embossed then ... else end end
Note, ENUM
's item
member has to be used with inspect
because the compiler does not recognized the psuedo Enum types are constants or the fact that they can be implicitly converted to a constant value.
ENUM
also has a feature call items
containing all available members of the Enum. With this code can be created an shown in the previous section. Pseudo Enum types, however, there is no good string representation of each member. Using ENUM.out
will yield the numerical value of the Enum.
It's a start.