CSC/ECE 517 Fall 2007/wiki1 3 b6: Difference between revisions
No edit summary |
No edit summary |
||
Line 27: | Line 27: | ||
== Example Program in Ruby == | === Example Program in Ruby === | ||
The following pair of programs demonstrates the value of currying functions in Ruby. The program itself implements a simple account balance management tool which allows for withdrawals and deposits as specified by the user. In the first program, a Logger class is defined with a single function, "logMessage" which takes an integer log level parameter, and the message to be logged. Depending on the log level specified, and the overall logging level set in the Logger class, the message may or may not be displayed to the user. If the message is to be displayed, it will be prefixed with a string indicating the message's relative importance. | The following pair of programs demonstrates the value of currying functions in Ruby. The program itself implements a simple account balance management tool which allows for withdrawals and deposits as specified by the user. In the first program, a Logger class is defined with a single function, "logMessage" which takes an integer log level parameter, and the message to be logged. Depending on the log level specified, and the overall logging level set in the Logger class, the message may or may not be displayed to the user. If the message is to be displayed, it will be prefixed with a string indicating the message's relative importance. | ||
Line 34: | Line 34: | ||
== Sample Program without Currying == | ==== Sample Program without Currying ==== | ||
Here is the initial version of the program: | Here is the initial version of the program: | ||
Line 119: | Line 119: | ||
== Limitations of the Initial Program == | ==== Limitations of the Initial Program ==== | ||
In the initial form of the program, the programmer must specify the log level parameter for each trace statement they wish to create. This approach has two drawbacks: | In the initial form of the program, the programmer must specify the log level parameter for each trace statement they wish to create. This approach has two drawbacks: | ||
Line 129: | Line 129: | ||
== Enhancing the program via Currying == | ==== Enhancing the program via Currying ==== | ||
Now the same program, but with 4 new methods defined by currying the original logMessage method. The new methods are: | Now the same program, but with 4 new methods defined by currying the original logMessage method. The new methods are: | ||
Line 152: | Line 152: | ||
== Improved Version of the Program == | ==== Improved Version of the Program ==== | ||
Line 266: | Line 266: | ||
== Conclusion == | ==== Conclusion ==== | ||
Notice how much easier it is to follow the flow of the program now through the clear labeling of debug/print/warning/error statements. It is very clear which output will be shown to the user and which is only provided for debugging purposes. | Notice how much easier it is to follow the flow of the program now through the clear labeling of debug/print/warning/error statements. It is very clear which output will be shown to the user and which is only provided for debugging purposes. |
Revision as of 03:57, 16 September 2007
Example use of Currying in Ruby by Ben Parees
This is an original work, the sample code provided was created entirely by the author of this page. References are provided to offer additional background on the concept of currying.
Definition of Function Currying
Currying refers to a programming technique in which the number of parameters to a function are reduced by creating a new function based on the original, but fixing some of the original arguments. That is, if we have a function f(x,y,z), we can define a new function g(x) which is defined as f(x,1,2).
Now f(x,1,2)==g(x), allowing us to invoke the simpler g(x) rather than f(x,1,2). To curry a function means to create the simplified function with fixed arguments, from a function with more arguments.
Benefits of Performing Function Currying
Currying has several benefits.
- It simplifies writing code by reducing the number of arguments that must be typed each time
- It reduces the risk of error because there are fewer arguments for which values could be incorrect
- It can enhance code readability by defining multiple functions with descriptive names rather than a single function with obscure arguments
Currying in Ruby
Ruby provides for function currying using the lambda operator. The lambda operator allows the programmer to define a new function. To curry g(x) from f(x,1,2) in Ruby would be written as: g=lambda {|x| f(x,1,2)} This would assign variable g to be a new function which invokes f() with the final two arguments fixed as 1 and 2 respectively.
Example Program in Ruby
The following pair of programs demonstrates the value of currying functions in Ruby. The program itself implements a simple account balance management tool which allows for withdrawals and deposits as specified by the user. In the first program, a Logger class is defined with a single function, "logMessage" which takes an integer log level parameter, and the message to be logged. Depending on the log level specified, and the overall logging level set in the Logger class, the message may or may not be displayed to the user. If the message is to be displayed, it will be prefixed with a string indicating the message's relative importance.
The purpose of this Logger class is to allow programmers to easily reconfigure their program to provide varying degrees of debugging output without changing code. They can simply change the log level that is configured, and automatically filter out messages which are below that log level. In addition, messages are automatically tagged with a string indicating their severity, which makes analyzing logs much easier. Unfortunately, the logMessage method is fairly cumbersome due to the requirement that the programmer specify the log level at each invocation.
Sample Program without Currying
Here is the initial version of the program:
class Logger @logLevel attr_accessor :logLevel def initialize(n) @logLevel=n end def logMessage(n,msg) if(n>=@logLevel) prepend=case n when 0 then 'DEBUG: ' when 1 then '' when 2 then 'WARNING: ' when 3 then 'ERROR: ' end puts prepend+msg end end end class Account @balance @logger attr_reader :balance def initialize(n,logger) @balance=n @logger=logger logger.logMessage(0,"initialized account with balance of #{n}"); end def getAmount @logger.logMessage(0,"enter getAmount method") amount=gets.chomp!.to_f @logger.logMessage(0,"Got amount #{amount}") if(amount<0) @logger.logMessage(3,"Amount is less than zero, amount will be set to zero") amount=0 end return amount end def withdraw @logger.logMessage(0,"enter withdraw method") @logger.logMessage(1,"How much would you like to withdraw?") amount=getAmount @logger.logMessage(0,"withdrawing amount of #{amount}") @balance=@balance-amount if(@balance<0) @logger.logMessage(2,"Account is overdrawn") end @logger.logMessage(0,"new balance is #{@balance}") end def deposit @logger.logMessage(0,"enter depost method") amount=getAmount @logger.logMessage(0,"depositing amount of #{amount}") @balance=@balance+amount @logger.logMessage(0,"new balance is #{balance}") end end action="d" logger=Logger.new(0) account=Account.new(100,logger) while action!="q" logger.logMessage(1,"Balance=#{account.balance}, would you like to withdraw, deposit, or quit [w/d/q]?") action=gets.chomp! logger.logMessage(0,"user chose action #{action}") if action=="w" account.withdraw elsif action=="d" account.deposit elsif action=="q" logger.logMessage(1,"goodbye") end end
Limitations of the Initial Program
In the initial form of the program, the programmer must specify the log level parameter for each trace statement they wish to create. This approach has two drawbacks:
- The programmer must know what log level corresponds to what level of trace (0=debug, 1=info, 2=warning, 3=error, etc). If they make a mistake in their numbering, the trace will not be correct.
- The code is hard to read since it is not obvious just by looking at a piece of code, whether a message is going to be printed as a debug statement, informational statement, warning, etc.
Enhancing the program via Currying
Now the same program, but with 4 new methods defined by currying the original logMessage method. The new methods are:
- debug - print a debug level message by currying with logMessage(0,msg)
- print - print an informational message by currying with logMessage(1,msg)
- warn - print a warning level message by currying with logMessage(2,msg)
- error - print an error level message by currying with logMessage(3,msg)
The actual currying is done in the Logger class, and the invocation of these methods can be seen in the body of the main program and the Account class.
Here is the section of code which performs the currying:
def initialize(n) @logLevel=n @debug=lambda{|msg| logMessage(0,msg)} @print=lambda{|msg| logMessage(1,msg)} @warn= lambda{|msg| logMessage(2,msg)} @error=lambda{|msg| logMessage(3,msg)} end
Improved Version of the Program
And here is the entire program rewritten to utilize the new methods.
class Logger @logLevel @debug @print @warn @error attr_accessor :logLevel def initialize(n) @logLevel=n @debug=lambda{|msg| logMessage(0,msg)} @print=lambda{|msg| logMessage(1,msg)} @warn= lambda{|msg| logMessage(2,msg)} @error=lambda{|msg| logMessage(3,msg)} end def logMessage(n,msg) if(n>=@logLevel) prepend=case n when 0 then 'DEBUG: ' when 1 then '' when 2 then 'WARNING: ' when 3 then 'ERROR: ' end puts prepend+msg end end def debug(msg) @debug.call(msg) end def print(msg) @print.call(msg) end def warn(msg) @warn.call(msg) end def error(msg) @error.call(msg) end end class Account @balance @logger attr_reader :balance def initialize(n,logger) @balance=n @logger=logger logger.debug("initialized account with balance of #{n}"); end def getAmount @logger.debug("enter getAmount method") amount=gets.chomp!.to_f @logger.debug("Got amount #{amount}") if(amount<0) @logger.error("Amount is less than zero, amount will be set to zero") amount=0 end return amount end def withdraw @logger.debug("enter withdraw method") @logger.print("How much would you like to withdraw?") amount=getAmount @logger.debug("withdrawing amount of #{amount}") @balance=@balance-amount if(@balance<0) @logger.warn("Account is overdrawn") end @logger.debug("new balance is #{@balance}") end def deposit @logger.debug("enter depost method") amount=getAmount @logger.debug("depositing amount of #{amount}") @balance=@balance+amount @logger.debug("new balance is #{balance}") end end action="d" logger=Logger.new(0) account=Account.new(100,logger) while action!="q" logger.print("Balance=#{account.balance}, would you like to withdraw, deposit, or quit [w/d/q]?") action=gets.chomp! logger.debug("user chose action #{action}") if action=="w" account.withdraw elsif action=="d" account.deposit elsif action=="q" logger.print("goodbye") end end
Conclusion
Notice how much easier it is to follow the flow of the program now through the clear labeling of debug/print/warning/error statements. It is very clear which output will be shown to the user and which is only provided for debugging purposes.
In addition, observe how simple it would be to add a new trace statement at any log level simply by invoking the desired method rather than having to look up the appropriate log level value.
References
--Bmparees 00:48, 12 September 2007 (EDT)