Hi folks, today I wanna share something that drew my attention while reading Confident Ruby.

Avdi presents the following method:

def delete_files ( files , ignore_errors = false , log_errors = false ) files . each do | file | begin File . delete ( file ) rescue => error puts error . message if log_errors raise unless ignore_errors end end end

And its invocation:

require 'fileutils' FileUtils . touch 'does_exist' delete_files ([ 'does_not_exist' , 'does_exist' ], true , true )

This code is not good at all.

the method worries to much in handling edge cases (error, logs)

The true, true in calls to delete_files does not help. We have to refer to the method definition to remember what those flags mean.

in calls to does not help. We have to refer to the method definition to remember what those flags mean. What if we wanted to log using a special format? Or to somewhere other than STDOUT? There is no provision for customizing how errors are logged.

What do we do if we ever decide to handle permissions errors differently from no such file or directory ? Add yet another flag?

The book point us the fundamental problem: both ignore_errors and log_errors attempt to specify policies using data.

Let's fix this by passing a block as argument:

def delete_files ( files , & error_policy ) error_policy ||= -> ( file , error ) { raise error } files . each do | file | begin File . delete ( file ) rescue => error error_policy . call ( file , error ) end end end

delete_files ([ 'does_not_exist' , 'does_exist' ]) do | file , error | puts error . message end

Look how flexible it is now. We define a default behavior for error handling but also let it open for customization by calling a given block.

What if we need more control over the behavior? Just pass more policies:

def delete_files ( files , options = {}) error_policy = options . fetch ( :on_error ) { -> ( file , error ) { raise error } } symlink_policy = options . fetch ( :on_symlink ) { -> ( file ) { File . delete ( file ) } } files . each do | file | begin if File . symlink? ( file ) symlink_policy . call ( file ) else File . delete ( file ) end rescue => error error_policy . call ( file , error ) end end end

delete_files ( [ 'file1' , 'file2' ], on_error: -> ( file , error ) { warn error . message }, on_symlink: -> ( file ) { File . delete ( File . realpath ( file )) } )

Conclusion

Block and proc as argument help callers to specify policies, freeing us from hard-coded decisions which someone else could make.