Reputation: 4062
normalize: sum
| strategy normalizingSum |
strategy := sum collect: [ :each | each max: 0.0 ].
normalizingSum := strategy sum.
^strategy collect: [ :each |
normalizingSum strictlyPositive
ifTrue: [ each / normalizingSum ]
ifFalse: [ 1.0 / Action subclasses size ]
]
I want to make the above an instance method rather than having to pass sum into it explicitly. The issue is not so much that I can't make the method or I can't put it into the Array
class. It is just that coming from functional languages, I find myself adrift on exactly what is expected of me here.
From what I can tell, as a convention most Pharo methods work directly on instances, while in functional languages what one would do is define a function that is analogous to static methods in C#/Java.
One choice is to put the method into a metaclass in Pharo, but Pharo's syntax is poorly suited for this style of programming. It does not for example have the pipe operator which explains the heavy tilt towards instance methods in all the library code that I've observed.
Extending a standard library class by directly putting a method into it feels a bit wrong somehow to me. When it comes time to push the changes to Github how exactly will things go? Putting it directly into the Array
class feels like it will turn into a versioning nightmare eventually.
Another choice is to inherit from Array
. This might be fine for the problem I am doing now, but later I want to do a different problem and do not want to share implementations.
Should I put it in a trait and add the trait to the Array
?
Upvotes: 4
Views: 226
Reputation: 14858
The problem here comes from the fact that we are only considering one message that your collections should answer: #normalized
. In your code, the collection sum
is the object that needs to be normalized. Therefore it would be tempting to say sum normalized
. However, I wouldn't recommend adding #normalized
to Array
(or Collection
) because the logic of your particular normalization is not intrinsic to Array
: it depends on Action
, which looks as a concept that makes sense in the context of your project.
This doesn't mean that you should never extend Array
with your stuff. It only means that if your extension is not intrinsic, such a decision would require more thought.
In this case what I would suggest is analyzing whether collections like sum
in your project have any other behavior that is intrinsic to them. In that case, I would consider having a class to represent them and add to this class messages such as #normalized
, plus any other one relevant to these objects.
Just as an example, let's assume that your class is named StrategyCollection
. You could define in it
strategy
^sum collect: [:each | each max: 0.0].
where sum
is now an ivar of your class. Then you could define
normalized
strategy := self strategy.
normalizingSum := strategy sum.
^strategy collect: [:each |
normalizingSum strictlyPositive
ifTrue: [each / normalizingSum]
ifFalse: [1.0 / Action subclasses size]]
which, by the way, could be re-written as
normalized
| strategy normalizingSum |
strategy := self strategy.
normalizingSum := strategy sum.
^normalizingSum strictlyPositive
ifTrue: [strategy collect: [:each | each / normalizingSum]]
ifFalse: [strategy class new: sumstrategy size withAll: 1.0 / Action subclasses size]
If you have other strategies besides #max: 0
, you could easily tweak the code above so to make it more general by using perform:
or having a special subclass of StrategyCollection
which implements its own version of #strategy
.
I would also suggest adding a method for the expression Action subclasses size
. Something on the lines of
actionCount
^Action subclasses size
and then use it in #normalized
. The expression Action subclasses size
is fragile and likely to appear in other methods. For instance, if tomorrow you decide to group some subclasses of Action
under another abstract subclass, the number of subclasses of Action
would not adapt to such a refactoring. More generally, the behavior of your objects should not depend on the way you organize your code, because that belongs in a meta-level of abstraction.
Upvotes: 4
Reputation: 13386
You should definitely make it an instance of Array
or even better of Collection
as your code works on anything that can be iterated (and has numbers).
The reason is that it's more clear to use:
#(3 5 49 3 1) normalized
rather than:
SomeMisteriousThirdParty normalize: #(3 5 49 3 1)
Additionally, if you have some special collections or other classes they can define their own versions of normalized
to handle things correctly.
The philosophy of Pharo is that no one can create a perfect environment exactly for your project. Thus it is easy to change the already available libraries to suit your needs.
The way to do this is to use extension methods where your package extends some other class with a method. This functionality is present in Pharo (and Smalltalk in general) for more than a decade, exactly for this reason when you need to extend another class but version the change together with your code.
While we are on the topic of normalization, it's worth mentioning that there are at least two big Projects that most likely need normalization to do what they are doing. One is Roassal used for data visualization and one is Polymath used for various computations. You may benefit from taking a look at what do they do.
Upvotes: 1