Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up

Catalog of Elixir Refactorings

License

NotificationsYou must be signed in to change notification settings

lucasvegi/Elixir-Refactorings

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GitHub last commitTwitter URL

Table of Contents

Introduction

Elixir is a functional programming language whose popularity is on the rise in the industrylink. As no known studies have explored refactoring strategies for code implemented with this language, we reviewed scientific literature seeking refactoring strategies in other functional languages. The found refactorings were analyzed, filtering only those directly compatible or that could be adapted for Elixir code. As a result of this investigation, we have initially proposed a catalog of 55 refactorings for Elixir systems.

Afterward, we scoured websites, blogs, forums, and videos (grey literature review), looking for specific refactorings for Elixir that its developers discuss. With this investigation, the catalog was expanded to 76 refactorings. Finally, 6 new refactorings emerged from a study mining software repositories (MSR) performed by us, so this catalog is constantly being updated andcurrently has 82 refactorings. These refactorings are categorized into four different groups (Elixir-specific,traditional,functional, andErlang-specific), according to the programming features required in code transformations. This catalog of Elixir refactorings is presented below. Each refactoring is documented using the following structure:

  • Name: Unique identifier of the refactoring. This name is important to facilitate communication between developers;
  • Category: Scope of refactoring in relation to its application coverage;
  • Motivation: Description of the reason why this refactoring should be done and what this refactoring does to the code;
  • Examples: Illustrates the resulting code from the refactoring, showing versions of it before and after the transformation;
  • Side-conditions (*): Minimum requirements to perform refactoring without creating conflicts with other parts of the code that may depend on the transformations promoted;
  • Mechanics (*): Sequence of main steps for the promoted code transformations.

Note: (*) not all refactorings have explicit definitions for these fields.

Tool support:RefactorEx is a VS Code extension inspired by this catalog that can semi-automatically apply some of the refactoring strategies defined here. Please take a look!

This catalog of refactorings aims to improve the quality of code developed in Elixir, helping developers promote the redesign of their code, making it simpler to understand, modify, or even improving performance. These transformations must be performed without changing the original behavior, thus preserving the code's functionality. For this reason, we are interested in knowing Elixir's community opinion about these refactorings:Do you agree that these refactorings can be useful? Have you seen any of them in production code? Do you have any suggestions about some Elixir-specific refactorings not cataloged by us?...

Please feel free to make pull requests and suggestions (Issues tab). We want to hear from you!

▲ back to Index

Elixir-Specific Refactorings

Elixir-specific refactorings are those that use programming features unique to this language. In this section, 14 different refactorings classified as Elixir-specific are explained and exemplified:

Alias expansion

  • Category: Elixir-specific Refactorings.

  • Source: This refactoring emerged from a Grey Literature Review (GLR).

  • Motivation: In Elixir, when using analias for multiple names from the same namespace, you can sort and consolidate them within a single file, reducing redundancy. Although this programming practice is common and can reduce the number of lines of code, multi-aliases can make it harder to search for a dependency in large code bases. This refactoring aims to expand multi-alias instructions fused into one multi-instruction per namespace, transforming them into single alias instructions per name. This provides improvement in code readability and traceability.

  • Examples: In the following code, before refactoring, we have a multi-alias instruction combining the definition of two dependencies. In this particular case, the dependencies for theBaz andBoom modules were merged into a single instruction.

    # Before refactoring:aliasFoo.Bar.{Baz,Boom}

    Especially in larger code bases, involving a greater number of dependencies within the same namespace (nested modules), the definition of these aliases could be refactored by analias expansion, better highlighting all dependencies, as shown in the following code.

    # After refactoring:aliasFoo.Bar.BazaliasFoo.Bar.Boom

    This example is derived from code found in the official documentation for the toolsRecode andExactoKnife.

  • Side-conditions:

    • The number ofalias commands inserted by this refactoring is identical to the number of modules that were originally merged into a singlealias instruction (i.e., inside a{}, such as{Baz, Boom});

    • Each of thealias instructions inserted by this refactoring should start with the path that was shared by the modules originally merged (e.g.,Foo.Bar), followed by a. and the name of one of the modules that were previously imported by a single command (e.g.,Bar orBoom).

▲ back to Index


Default value for an absent key in a Map

  • Category: Elixir-specific Refactorings.

  • Source: This refactoring emerged from a Grey Literature Review (GLR).

  • Motivation: We often come across a situation where we expect aMap to have a certain key, and if not, we need to provide a default value. A commonly used alternative for such situations is using the built-inMap.has_key?/2 function along with anif...else statement. Although this alternative works perfectly, it's possible to refactor this code using only the built-inMap.get/3 function, making the code less verbose and more readable, while preserving the same behavior.

  • Examples: In the following code, before refactoring, we utilize theMap.has_key?/2 function in conjunction with anif...else statement to retrieve the currency from aMap containing the price of a product. If thecurrency key does not exist, we return the default value of"USD".

    # Before refactoring:currency=if(Map.has_key?(price,"currency"))doprice["currency"]else"USD"

    Applying this refactoring, the above code can be transformed into the following code, preserving the behavior while reducing the number of lines.

    # After refactoring:currency=Map.get(price,"currency","USD")

    This example is based on an original code by Malreddy Ankanna. Source:link

  • Side-conditions:

    • To be eligible for this refactoring, the code snippet must consist of anif..else statement, where the condition checked is a call to the built-in functionMap.has_key?/2. Additionally, the branch created by theif should only return the value of the key in theMap whose existence was checked by the call toMap.has_key?/2, while the branch defined by theelse should only return a default value for a non-existent key.

▲ back to Index


Defining a subset of a Map

  • Category: Elixir-specific Refactorings.

  • Source: This refactoring emerged from a Grey Literature Review (GLR).

  • Motivation: When dealing with hugeMap structures, there are occasions when we need to extract a subset of elements to form a newMap. Instead of manually creating this subset by individually accessing each of the desired key/value pairs from the originalMap, with this refactoring, we can simply use the built-inMap.take/2 function.

  • Examples: In the following code, we have a variablepickup bound to aMap composed of a huge number of keys.

    # Huge Mappickup=%{"zip"=>"75010","town"=>"PARIS","stopName"=>"RECEPTION","pickupId"=>4018,"longitude"=>2.360982,"latitude"=>48.868502....#a lot of keys}

    To extract only the metadata related to location, before refactoring, we manually access the values identified by the keys"latitude" and"longitude" to then create a newMap.

    # Before refactoring:longitude=pickup["longitude"]latitude=pickup["latitude"]location=%{# <-- Defining a subset manually"longitude"=>longitude,"latitude"=>latitude}

    Although this solution works, it can generate a significant amount of code, primarily due to duplications. Applying this refactoring, we can eliminate duplicated code, significantly reducing the number of lines and improving readability.

    # After refactoring:location=Map.take(pickup,["latitude","longitude"])

    This example is based on an original code by Malreddy Ankanna. Source:link

  • Side-conditions:

    • To be eligible for this refactoring, the code snippet must originally create a subset of aMap manually, meaning it constructs a newMap containing only some of the key/value pairs from the completeMap;

    • The number of elements in the list passed as the second parameter of theMap.take/2 call in the refactored version of the code must be identical to the number of key/value pairs that originally composed the manually created subset. Additionally, the elements of this list must be exactly the keys of that subset.

▲ back to Index


Modifying keys in a Map

  • Category: Elixir-specific Refactorings.

  • Source: This refactoring emerged from a Grey Literature Review (GLR).

  • Motivation: Sometimes we need to update the format of aMap, replacing the name given to a key but keeping the new key name associated with the original value. Instead of usingMap.get/2,Map.put/2, andMap.delete/2 functions together, which involves a lot of manual work and generates many lines of code, we can simply use the built-inMap.new/2 function along with the use of multi-clause lambdas. This refactoring can significantly reduce the volume of lines of code, eliminating duplicated code and even making the code more resilient to typing-related errors.

  • Examples: In the following code, we have a variablepickup bound to aMap.

    pickup=%{"stopName"=>"RECEPTION","pickupId"=>4018,"longitude"=>2.360982,"latitude"=>48.868502}

    Let's suppose we want to change the name of the key"latitude" to simply"lat", while keeping the rest of theMap unchanged. The following code, before refactoring, performs this task manually. It first retrieves the value associated with the"latitude" key, then creates a new key called"lat" and associates it with the extracted value from the"latitude" key. Finally, the"latitude" key is removed from theMap.

    # Before refactoring:latitude=Map.get(pickup,"latitude")# --> step 1pickup=Map.put(pickup,"lat",latitude)# --> step 2pickup=Map.delete(pickup,"latitude")# --> step 3

    Although this solution works, imagine a situation where many keys need to be updated in aMap. The manual strategy presented above can become cumbersome and impractical. By using the built-inMap.new/2 function, this refactoring would make it easier to simultaneously update the format of all keys in theMap, as shown in the following code.

    # After refactoring:pickup=Map.new(pickup,fn{"latitude",lat}->{"lat",lat}{key,value}->{key,value}# <-- Clause for unchanged keysend)

    This example is based on an original code by Malreddy Ankanna. Source:link

  • Side-conditions:

    • To be eligible for this refactoring, the code snippet must originally be composed of a call to the functionMap.get/2, followed byMap.put/2, andMap.delete/2;

    • All temporary variables created in the refactored version for performing pattern matching in clauses of the multi-clause anonymous function (e.g.,lat,key, andvalue) must have names different from other previously defined variables, thus avoiding conflicts;

    • The multi-clause anonymous function passed as a parameter to theMap.new/2 call in the refactored version must have at least two clauses: one for each modified key (e.g.,{"latitude", lat} -> {"lat", lat}) and another to keep all other keys unchanged as in the original (i.e.,{key, value} -> {key, value}).

▲ back to Index


Simplifying Ecto schema fields validation

  • Category: Elixir-specific Refactorings.

  • Source: This refactoring emerged from a Grey Literature Review (GLR).

  • Motivation: After defining a schema in Ecto, it's common to need to group fields for validations, such as those performed by the Ecto validatorvalidate_required/3. However, if we attempt to perform this grouping by manually implementing a list, there's a risk of making the code overly verbose, prone to typographical errors, and even subject to rework if the schema is modified in the future. Instead of relying on manually created lists, we can simply use the Ecto__schema__/1 function, which returns the list of fields in the schema. With this refactoring, we can simplify the code, making it less prone to errors and more maintainable.

  • Examples: In the following code, we have an Ecto schema composed by six fields.

    embedded_schemadofield:carrier_time,:stringfield:carrier_date,:stringfield:carrier_name,:stringfield:carrier_number,:stringfield:carrier_terminal,:stringfield:carrier_type,:stringend

    The following code manually lists in theschema_fields variable all the fields in our schema that will be validated by the Ectovalidate_required/3 function. Note that this listing process can be cumbersome, prone to typographical errors, and furthermore, it generates duplicated code.

    # Before refactoring:defchangeset(attrs)do# Manual listing of schema fieldsschema_fields=[:carrier_time,:carrier_date,:carrier_name,:carrier_number,:carrier_terminal,:carrier_type]%__MODULE__{}|>cast(attrs,schema_fields)|>validate_required(schema_fields,message:"Missing Field")end

    We can refactor the field listing by replacing the manual list with a call to the Ecto__schema__/1 function. When we call this function with the atom:fields as a parameter, it returns the list of all non-virtual field names, which is exactly the same list we created manually before the refactoring.

    # After refactoring:defchangeset(attrs)doschema_fields=__schema__(:fields)#<-- returns dynamically the list of schema fields!%__MODULE__{}|>cast(attrs,schema_fields)|>validate_required(schema_fields,message:"Missing Field")end

    Although simple, this refactoring brings many improvements to the code quality. If the database schema is altered, for instance, the above code will continue to work for all fields in the schema without the need for additional modifications.

    This example is based on an original code by Malreddy Ankanna. Source:link

  • Side-conditions:

    • To be eligible for this refactoring, the code snippet must originally contain a manually created list being passed as a parameter to a call to thevalidate_required/3 function;

    • If the original list does not contain all the fields of an Ecto schema, the refactored version should perform a list subtraction (i.e.,Kernel.--/2) on the result of the__schema__/1 function call. For example, if the original list includes all fields except:carrier_terminal and:carrier_type, to maintain the same behavior, the following expression should be used:__schema__(:fields) -- [:carrier_terminal, :carrier_type].

▲ back to Index


Pipeline using "with"

  • Category: Elixir-specific Refactorings.

  • Source: This refactoring emerged from a Grey Literature Review (GLR).

  • Motivation: When conditional statements, such asif..else andcase, are used in a nested manner to create sequences of function calls, the code can become confusing and have poor readability. In these situations, we can replace the use of nested conditionals with a kind of function call pipeline using awith statement. This refactoring enforces the use of pattern matching at each function call, interrupting the pipeline if any pattern does not match. Additionally, it has the potential to enhance code readability without modifying the signatures (heads) of the involved functions, making this refactoring less prone to breaking changes compared toConvert nested conditionals to pipeline.

  • Examples: In the following code, the functionupdate_game_state/3 uses nested conditional statements to control the flow of a sequence of function calls tovalid_move/2,players_turn/2, andplay_turn/3. All these sequentially called functions have a return pattern of{:ok, _} or{:error, _}, which is common in Elixir code.

    # Before refactoring:defpupdate_game_state(%{status::started}=state,index,user_id)do{move,_}=valid_move(state,index)ifmove==:okdoplayers_turn(state,user_id)|>casedo{:ok,marker}->play_turn(state,index,marker)other->otherendelse{:error,:invalid_move}endend

    Note that, although this code works perfectly, the nesting of conditionals used to ensure the safe invocation of the next function in the sequence makes the code confusing. Therefore, we can refactor it by replacing these nested conditional statements with a pipeline that uses awith statement, thereby reducing the number of lines of code and improving readability, while keeping the behavior and signature of all the involved functions intact.

    # After refactoring:defpupdate_game_state(%{status::started}=state,index,user_id)dowith{:ok,_}<-valid_move(state,index),{:ok,marker}<-players_turn(state,user_id),{:ok,new_state}<-play_turn(state,index,marker)do{:ok,new_state}else(other->other)endend

    As is characteristic of thewith statement, the next function in this pipeline will only be called if the pattern of the previous call matches. Otherwise, the pipeline is terminated, returning the error that prevented it from proceeding to completion. Note that this refactored version, although functioning correctly, also presents an opportunity to apply the refactoringRemove redundant last clause in "with", since the last clause of thewith statement is composed of a pattern identical to the predefined value to be returned by thewith in case all checked patterns match.

    This example is based on an original code by Gary Rennie. Source:link

  • Side-conditions:

    • To be eligible for this refactoring, each of the functions called sequentially within a nested conditional must originally have their execution flow controlled by some form of pattern matching. For example, functions that return values in the format{:ok, _} or{:error, _} are candidates for having their sequential calls refactored;

▲ back to Index


Pipeline for database transactions

  • Category: Elixir-specific Refactorings.

  • Source: This refactoring emerged from a Grey Literature Review (GLR).

  • Motivation: TheEcto.Repo.transaction/2 function allows performing operations on the database, such as update and delete. The first parameter of this function can be an anonymous function or a data structure calledEcto.Multi. When we want to perform a sequence of operations on the database using only one call toEcto.Repo.transaction/2, the use of an anonymous function as the first parameter of this function can impair code readability, making it confusing. This refactoring converts anonymous functions used to create a pipeline of operations into calls toEcto.Repo.transaction/2, transforming them into instances ofEcto.Multi, a data structure used for grouping multiple Repo operations. The code generated by this refactoring becomes cleaner and, furthermore, it does not allow the execution of operations if theEcto.Multi is invalid (i.e., if any of the changesets have errors).

  • Examples: In the following code, the functionclear_challenges/2 makes a call toEcto.Repo.transaction/2 aiming to execute a sequence of update and delete operations on the database.

    # Before refactoring:defupdate_refused_challenges(user,count)doparams=%{refused_challenges:user.refused_challenges+count}user|>User.update_changeset(params)|>Repo.update()enddefpdelete_challenges(challenges)doids=Enum.map(challenges,&(&1.id))query=where(Challenge,[c],c.idin^ids)Repo.delete_all(query)enddefpstop_games(challenges)doEnum.map(challenges,&GameRegistry.delete_game(&1.id))enddefclear_challenges(user,age\\300)dochallenges=get_old_open_challenges(user,age)count=length(challenges)Repo.transaction(fn->with{:ok,user}<-update_refused_challenges(user,count),delete_challenges(challenges),stop_games(challenges),do:user,else:({:error,reason}->Repo.rollback(reason))end)end

    Note that before the refactoring, this call toEcto.Repo.transaction/2 uses a complex anonymous function as its first parameter. This anonymous function employs awith statement to structure a pipeline of operations, similar toPipeline using "with". While this code works perfectly, in this context, we can enhance the readability of this database operations pipeline by replacing the anonymous function with theEcto.Multi data structure, specifically designed for creating such pipelines. The following code presents the refactored version of theclear_challenges/2 function.

    # After refactoring:defupdate_refused_challenges(user,count)doparams=%{refused_challenges:user.refused_challenges+count}user|>User.update_changeset(params)#|> Repo.update()   <-- removed during refactoring!enddefpdelete_challenges(challenges)doids=Enum.map(challenges,&(&1.id))query=where(Challenge,[c],c.idin^ids)# Repo.delete_all(query) <-- removed during refactoring!enddefpstop_games(challenges)doEnum.map(challenges,&GameRegistry.delete_game(&1.id)){:ok,:stopped_games}# <--- added during refactoring!enddefclear_challenges(user,age\\300)dochallenges=get_old_open_challenges(user,age)count=length(challenges)Ecto.Multi.new()|>Ecto.Multi.update(:user,update_refused_challenges(user,count))|>Ecto.Multi.delete_all(:challenges,delete_challenges(challenges))|>Ecto.Multi.run(:games,fn_->stop_games(challenges)end)|>Repo.transaction()end

    This example is based on an original code by Gary Rennie. Source:link

▲ back to Index


Transform nested "if" statements into a "cond"

  • Category: Elixir-specific Refactorings.

  • Source: This refactoring emerged from a Grey Literature Review (GLR).

  • Motivation: While code using nestedif statements works, it can be verbose and not very maintainable in some situations. Elixir doesn’t have anelse if construct, but it does have a statement calledcond that is logically equivalent. This refactoring aims to transform multiple conditionals, implemented using nestedif statements, into the use of acond statement, leaving the code without complex indentations and therefore cleaner and more readable.

  • Examples: In the following code, theclassify_bmi/2 function uses several nestedif..else statements to classify the Body Mass Index (BMI) of an individual, calculated based on their weight and height.

    # Before refactoring:defclassify_bmi(weight,height)do{status,bmi}=calculate_bmi(weight,height)ifstatus==:okdoifbmi<18.5do"Underweight"elseifbmi<25.0do"Normal weight"elseifbmi<30.0do"Overweight"elseifbmi<35.0do"Obesity grade 1"elseifbmi<40.0do"Obesity grade 2"else"Obesity grade 3"endendendendendelse"Error in BMI calculation:#{bmi}"endend

    Although this code works well, it is unnecessarily large in terms of the number of lines, and it also has complex indentations, resulting in an unattractive and less maintainable appearance. In the following code, after refactoring the nestedif..else statements into an Elixircond statement, theclassify_bmi/2 function has a cleaner and more readable appearance.

    # After refactoring:defclassify_bmi(weight,height)do{status,bmi}=calculate_bmi(weight,height)ifstatus==:okdoconddobmi<18.5->"Underweight"bmi<25.0->"Normal weight"bmi<30.0->"Overweight"bmi<35.0->"Obesity grade 1"bmi<40.0->"Obesity grade 2"true->"Obesity grade 3"endelse"Error in BMI calculation:#{bmi}"endend

▲ back to Index


Explicit a double boolean negation

  • Category: Elixir-specific Refactorings.

  • Source: This refactoring emerged from a Grey Literature Review (GLR).

  • Motivation: In Elixir, when we perform a double boolean negation, we cast anything truthy totrue and anything non-truthy tofalse. In other words, this will returnfalse forfalse andnil, andtrue for anything else. Although this approach may seem interesting initially, it can make the code less expressive by omitting the real intention of this operation. Therefore, to improve readability, we can replace double boolean negations by introducing a helper multi-clause function.

  • Examples: In the following code, we can observe the behavior of double boolean negations applied to four distinct variables.

    # Before refactoring:var_1=truevar_2=falsevar_3=nilvar_4=100#...Use examples...iex(1)>!!var_1trueiex(2)>!!var_2falseiex(3)>!!var_3falseiex(4)>!!var_4true

    To make our code more expressive, we can refactor the operations above by creating a multi-clause function that uses pattern matching to map all the behavioral possibilities of a double boolean negation. Below, we demonstrate this refactoring by creating the functionhelper/1. Note that this name is purely illustrative, so the function could be renamed to something that better represents its purpose, depending on the context.

    # After refactoring:defmoduleFoododefhelper(nil),do:falsedefhelper(false),do:falsedefhelper(_),do:trueend#...Use examples...iex(1)>Foo.helper(var_1)trueiex(2)>Foo.helper(var_2)falseiex(3)>Foo.helper(var_3)falseiex(4)>Foo.helper(var_4)true

    These examples are based on code written in Credo's official documentation. Source:link

  • Side-conditions:

    • The name and arity of function created in this refactoring (e.g.,helper/1) must not conflict with the name of any other function already defined or imported by the refactored module.

▲ back to Index


Transform "if" statements using pattern matching into a "case"

  • Category: Elixir-specific Refactorings.

  • Source: This refactoring emerged from a Grey Literature Review (GLR).

  • Motivation: Pattern matching is most effective for simple assignments withinif andunless clauses. Although Elixir allows pattern matching in conditional tests performed by anif statement, it may compromise code readability when used for flow control purposes. If you need to match a condition and execute a different block when it's not met, it's advisable to use acase statement instead of combining pattern matching with anif statement. This refactoring, therefore, aims to carry out this type of code transformation.

  • Examples: In the following code, anif statement is used in conjunction with pattern matching. In this situation, thedo_something/1 function is called if the pattern matches.

    # Before refactoring:if{:ok,contents}=File.read("foo.txt")dodo_something(contents)end

    As shown in the following code, we can refactor the previous code by replacingif statements that use pattern matching with an Elixircase statement, which is a more appropriate conditional for working alongside pattern matching. This refactoring also makes the code more flexible for future changes, as it opens the possibility to execute different code blocks when a pattern does not match, something that would not be possible using only anif..else statement.

    # After refactoring:caseFile.read("foo.txt")do{:ok,contents}->do_something(contents)_->do_something_else()end

    These examples are based on code written in Credo's official documentation. Source:link

  • Side-conditions:

    • This refactoring is free of side-conditions and can therefore be applied whenever anif statement performing pattern matching in its condition is selected.

▲ back to Index


Moving "with" clauses without pattern matching

  • Category: Elixir-specific Refactorings.

  • Source: This refactoring emerged from a Grey Literature Review (GLR).

  • Motivation: Usingwith statements is recommended when you want to string together a series of pattern matches, stopping at the first one that fails. Although is possible to define awith statement using an initial or final clause that doesn't involve a<- operator (i.e., it doesn't match anything), it fails to leverage the advantages provided by thewith, potentially causing confusion. This refactoring aims to move these clauses that don't match anything to outside thewith statement (for the initial ones) or place them inside the body of thewith statement (for the final ones), thereby enhancing the code's focus and readability.

  • Examples: In the following code, we have awith statement composed of four clauses. As we can observe, the first and last clauses do not involve matching specific patterns. In other words, they do not use the<- operator.

    # Before refactoring:withref=make_ref(),{:ok,user}<-User.create(ref),:ok<-send_email(user),Logger.debug("Created user:#{inspect(user)}")douserend

    To enhance the readability of our code, keeping the clauses of thewith statement focused solely on performing a pipeline of pattern matching, we can move the first clause of this code outside of thewith statement and the last clause to inside its body, as shown in the following code. Note that although these clauses have been moved, the behavior of the code remains unchanged.

    # After refactoring:ref=make_ref()# moved outside of the 'with'with{:ok,user}<-User.create(ref),:ok<-send_email(user)doLogger.debug("Created user:#{inspect(user)}")# moved inside the body of the 'with'userend

    These examples are based on code written in Credo's official documentation. Source:link

  • Side-conditions:

    • The first and/or last clause of thewith statement to be refactored should not use the<- operator (i.e., they don't match anything).

▲ back to Index


Remove redundant last clause in "with"

  • Category: Elixir-specific Refactorings.

  • Source: This refactoring emerged from a Mining Software Repositories (MSR) study.

  • Motivation: When the last clause of anwith statement is composed of a pattern identical to the predefined value to be returned by thewith in case all checked patterns match, this clause is consideredredundant. In such situations, this last clause of thewith can be removed, and the predefined value to be returned by thewith should then be replaced by the expression that was checked in the redundant clause that was removed. This refactoring will maintain the same behavior of the code while making it less verbose and more readable.

  • Examples: In the following code, the callbackhandle_call/3 uses awith statement with a redundant last clause. Note that the pattern compared in the last clause is identical to the predefined value to be returned by thewith in case all checked patterns match:{:ok, conf}.

    # Before refactoring:defmodulePhoenix.LiveView.ChanneldouseGenServer...@impltruedefhandle_call({@prefix,:fetch_upload_config,name,cid},_from,state)doread_socket(state,cid,fnsocket,_->result=with{:ok,uploads}<-Map.fetch(socket.assigns,:uploads),{:ok,conf}<-Map.fetch(uploads,name)do#<- redundant last clause!{:ok,conf}#<- predefined value to be returned by the ``with``!end{:reply,result,state}end)end...end

    As demonstrated in the following code, we can refactor this by removing the redundant last clause{:ok, conf} <- Map.fetch(uploads, name) and also replacing the predefined value to be returned with the expressionMap.fetch(uploads, name), which was previously checked in the removed redundant clause.

    # After refactoring:defmodulePhoenix.LiveView.ChanneldouseGenServer...@impltruedefhandle_call({@prefix,:fetch_upload_config,name,cid},_from,state)doread_socket(state,cid,fnsocket,_->result=with{:ok,uploads}<-Map.fetch(socket.assigns,:uploads)doMap.fetch(uploads,name)#<- predefined value to be returned by the ``with``!end{:reply,result,state}end)end...end

    This example is based on an original code refactored by ByeongUk Choi. Source:link

  • Side-conditions:

    • The pattern checked in the last clause of thewith statement must be lexically identical to the value returned in the body of thewith. Additionally, the body of thewith should contain only the value to be returned.

▲ back to Index


Replace "Enum" collections with "Stream"

  • Category: Elixir-specific Refactorings.

  • Source: This refactoring emerged from a Mining Software Repositories (MSR) study.

  • Motivation: All the functions in theEnum module areeager. This means that when performing multiple operations withEnum, each operation will generate an intermediate collection (e.g.,lists ormaps) until we reach the result. On the other hand, Elixir provides theStream module which supportslazy operations, so instead of generating intermediate collections, streams build a series of computations that are invoked only when we pass the underlyingStream to theEnum module. This refactoring suggests using theStream module instead of theEnum modulewhen multiple operations in large collections are performed together. This can significantly decrease the time to traverse the collections while keeping the same behavior.

  • Examples: The code examples below illustrate this refactoring. Before the refactoring, the higher-order functionsum_odd_numbers/2 uses onlyEnum's functions to initially modify the values of alist, filter all modified values that are odd, and then sum them up.

    # Before refactoring:defmoduleFoododefsum_odd_numbers(list,odd?)dolist|>Enum.map(&(&1*3))|>Enum.filter(odd?)|>Enum.sum()endend#...Use examples...iex(1)>Foo.sum_odd_numbers([1,2,3,4,5,6,7,8],&(rem(&1,2)!=0))48

    Following the refactoring,sum_odd_numbers/2 retains the same behavior but now uses someStream functions instead ofEnum. Note that even after the refactoring, theEnum.sum/1 function, which is the last operation in the pipeline, was kept in the code. SinceStream module functions arelazy operations, the computations accumulated inStream.map/2 andStream.filter/2 are only invoked when thisStream is passed to a function from theEnum module, in this case theEnum.sum/1 function.

    # After refactoring:defmoduleFoododefsum_odd_numbers(list,odd?)dolist|>Stream.map(&(&1*3))#<- Replace "Enum" with "Stream"!|>Stream.filter(odd?)#<- Replace "Enum" with "Stream"!|>Enum.sum()endend#...Use examples...iex(1)>Foo.sum_odd_numbers([1,2,3,4,5,6,7,8],&(rem(&1,2)!=0))48

    By using theBenchee library for conducting micro benchmarking in Elixir, we can highlight the performance improvement potential of this refactoring. In the following code, thesum_odd_numbers/2 function is given illustrative names,before_ref/1 andafter_ref/1, to represent their respectiveEnum andStream versions.

    list=Enum.to_list(1..50_000_000)odd?=fnx->rem(x,2)!=0endBenchee.run(%{"enum"=>fn->Foo.before_ref(list,odd?)end,"stream"=>fn->Foo.after_ref(list,odd?)end},parallel:4,memory_time:2)

    Note that for a list with fifty million elements, theStream version, although it consumes more memory, can be abouttwelve times faster than theEnum version.

    Operating System: WindowsCPU Information: Intel(R) Core(TM) i7-2670QM CPU @ 2.20GHzNumber of Available Cores: 8Available memory: 11.95 GBElixir 1.16.0Erlang 26.2.1Benchmark suite executing with the following configuration:warmup: 2 stime: 5 smemory time: 2 sreduction time: 0 nsparallel: 4inputs: none specifiedEstimated total run time: 18 sBenchmarking enum ...Benchmarking stream ...Name             ips        average  deviation         median         99th %stream         0.144      0.116 min     ±1.63%      0.116 min      0.117 minenum          0.0123       1.36 min    ±49.35%       1.11 min       2.31 minComparison:stream         0.144enum          0.0123 - 11.76x slower +1.24 minMemory usage statistics:Name      Memory usagestream         2.05 GBenum           1.12 GB - 0.55x memory usage -0.93132 GB

    These examples are based on code written in Elixir's official documentation. Source:link

  • Side-conditions:

    • Function calls from theEnum module used in a pipeline can only be replaced by functions from theStream module that perform operations equivalent to the originals;

    • To maintain the behavior of the original code, after the calls to functions from theStream module inserted in this refactoring, there must be at least one call to a function from theEnum module in the pipeline.

▲ back to Index


Generalise a process abstraction

  • Category: Elixir-specific Refactorings.

  • Source: This refactoring emerged from a Mining Software Repositories (MSR) study.

  • Motivation: Elixir provides different types of process abstractions for distinct purposes. While theTask andAgent abstractions have very specific purposes,GenServer is a more generic process abstraction, therefore having the capability to do everything thatTask andAgent can do, as well as having additional capabilities beyond these two specific abstractions. This refactoring aims to transformTask orAgent abstractions intoGenServer when these specific-purpose abstractions are used beyond their suggested purposes. More specifically, this refactoring can be used to remove the code smellGenServer Envy. By using an appropriate process abstraction for the purpose of the code, we can even improve its readability.

  • Examples: In the following code, theDatabaseServer module makes use of aTask abstraction to provide its clients with the ability to query a database using the interface functionget/2. As can be observed in this example, thisTaskbehaves like a long-running server process, frequently communicating with other client processes. This behavior is very different from the suggested purpose forTasks, which typically should only perform a particular operation during their lifetime and then stop upon the completion of that operation without communication with other processes.

    # Before refactoring:defmoduleDatabaseServerdouseTaskdefstart_link()doTask.start_link(&loop/0)enddefploop()doreceivedo{:run_query,caller,query_def}->send(caller,{:query_result,run_query(query_def)})endloop()enddefget(server_pid,query_def)dosend(server_pid,{:run_query,self(),query_def})receivedo{:query_result,result}->resultendenddefprun_query(query_def)doProcess.sleep(1000)"#{query_def} result"endend#...Use examples...iex(1)>{:ok,pid}=DatabaseServer.start_link(){:ok,#PID<0.161.0>}iex(2)>DatabaseServer.get(pid,"query 1")"query1result"iex(3)> DatabaseServer.get(pid, "query2") "query2 result"

    Considering that the above code represents an instance of the code smellGenServer Envy, we can refactor it by generalizing the process abstraction used. In other words, we can transform this specific process abstraction (Task) into a generic process abstraction (GenServer). Note that although the process abstraction used has been replaced, the behavior of the code remains the same because the interfaces of the public functions have not been modified. Furthermore, the readability of the refactored code has improved, as it was no longer necessary to make explicit message passing and implement recursive functions to keep the process alive.

    # After refactoring:defmoduleDatabaseServerdouseGenServerdefstart_link()doGenServer.start_link(__MODULE__,nil)enddefget(server_pid,query_def)doGenServer.call(server_pid,{:run_query,query_def})enddefprun_query(query_def)doProcess.sleep(1000)"#{query_def} result"end@impldefhandle_call({:run_query,query_def},_,state)do{:reply,run_query(query_def),state}endend#...Use examples...iex(1)>{:ok,pid}=DatabaseServer.start_link(){:ok,#PID<0.164.0>}iex(2)>DatabaseServer.get(pid,"query 1")"query1result"iex(3)> DatabaseServer.get(pid, "query2") "query2 result"

    This example is based on an original code by Saša Jurić available in the"Elixir in Action, 2. ed." book.

▲ back to Index


Traditional Refactorings

Traditional refactorings are those mainly based on Fowler's catalog or that use programming features independent of languages or paradigms. In this section, 25 different refactorings classified as traditional are explained and exemplified:

Rename an identifier

  • Category: Traditional Refactorings.

  • Motivation: It's important to keep in mind that although giving good names to things may not be a simple task, good names for code structures are essential to facilitate maintenance activities promoted by humans. When the name of an identifier does not clearly convey its purpose, it should be renamed to improve the code's readability, thus avoiding a developer from wasting too much time trying to understand code developed by someone else or even developed by themselves a long time ago. In Elixir, code identifiers can befunctions,modules,macros,variables,map/struct fields, registered processes (e.g.GenServer),protocols,behaviours callbacks,module aliases,module attributes,function parameters, etc.

  • Examples: The following code illustrates this refactoring in the context ofrenaming functions. Before the refactoring, we have a functionfoo/2, which receives two parameters and returns their sum. Although it is a simple function, it is evident that its name does not clearly convey its purpose.

    # Before refactoring:deffoo(value_1,value_2)dovalue_1+value_2end

    We intend to rename this function tosum/2, thus highlighting its purpose. To do so, we should create a new function with this name and copy the body of thefoo/2 function to it. Additionally, the body of thefoo/2 function should be replaced by a call to the newsum/2 function:

    # After refactoring:defsum(value_1,value_2)dovalue_1+value_2enddeffoo(value_1,value_2)do#<-- must be deleted in the future!sum(value_1,value_2)end

    Thefoo/2 function acts as a wrapper that callssum/2 and should be kept in the code temporarily, only while the calls to it throughout the codebase are gradually replaced by calls to the newsum/2 function. This mitigates the risk of this refactoring generating breaking changes. When there are no more calls to thefoo/2, it should be deleted from its module.

  • Side-conditions:

    • The new name should not conflict with other pre-existing names;

    • In the specific case ofrenaming functions, the new function name should not conflict with other functions of the same module, nor with those imported from another module.

  • Mechanics: This sequence of steps may vary depending on the type of identifier that will be renamed. In the specific case ofrenaming functions, the sequence of steps for the transformation should be as follows.

    • Check if the function being renamed was not previously defined by an Elixirbehaviour orprotocol implemented by the module of the function;

      • If the name was originally defined in abehaviour orprotocol, these transformations should be promoted at the source of that function (behaviour orprotocol) and in all modules of the codebase that implement that source.
    • Create a new function with a name that better indicates its purpose and copy the body of the original function (with a bad name) into the new function;

    • Replace the body of the original function with a call to the new function;

    • Test your code to check for the occurrence of breaking changes;

    • For each call to the original function, replace it with a call to the new renamed function and test your code again;

    • After all calls to the original function have been replaced with the new function, and the code has been tested to verify that there are no breaking changes, it is safe to delete the original function and its definitions in abehaviour orprotocol (if applicable) to complete the refactoring process.

▲ back to Index


Moving a definition

  • Category: Traditional Refactoring.

  • Motivation: Modules in Elixir are used to group related and interdependent definitions, promoting cohesion. A definition in Elixir can be afunction,macro, orstruct, for example. When a definition accesses more data or calls more functions from another module other than its own, or is used more frequently by another module, we may have less cohesive modules with high coupling. To improve maintainability by grouping more cohesive definitions in modules, these definitions should be moved between modules when identified. This refactoring helps to eliminate theFeature Envy code smell.

  • Examples: The following code illustrates this refactoring in the context ofmoving functions. Before the refactoring, we have a functionfoo/2 fromModuleA, which besides not being called by any other function in its module, only makes calls to functions from another module (ModuleB). This functionfoo/2, as it is clearly misplaced inModuleA, decreases the cohesion of this module and creates an avoidable coupling withModuleB, making the codebase harder to maintain.

    # Before refactoring:defmoduleModuleAdoaliasModuleB,as:Bdeffoo(v1,v2)doB.baz(v1,v2)|>B.qux()enddefbar(v1)do...endend#...Use example...iex(1)>ModuleA.foo(1,2)
    # Before refactoring:defmoduleModuleBdodefbaz(value_1,value_2)do...enddefqux(value_1)do...endend

    We want to movefoo/2 toModuleB to improve the grouping of related functions in our codebase. To do this, we should not only copyfoo/2 toModuleB, but also check iffoo/2 depends on other resources that should also be moved or if it has references that need to be updated when the function is positioned in its new module.

    # After refactoring:defmoduleModuleAdodefbar(v1)do...endend
    # After refactoring:defmoduleModuleBdodefbaz(value_1,value_2)do...enddefqux(value_1)do...enddeffoo(v1,v2)do#<-- moved function!baz(v1,v2)|>qux()endend#...Use example...iex(1)>ModuleB.foo(1,2)

    All calls toModuleA.foo/2 should be updated toModuleB.foo/2. When there are no more calls toModuleA.foo/2 in the codebase, it should be deleted fromModuleA. In addition,ModuleA will no longer need to import functions fromModuleB, since this coupling has been undone.

  • Side-conditions:

    • When the moved definition is afunction ormacro, the name of this definition must not conflict with the name of any other definition of the same type already defined in the target module or imported by it;

    • When the moved definition is afunction that originally calls other functions or macros, after the refactoring, this moved function must still refer to the same definitions of functions and macros originally called;

    • When the moved definition is astruct, it can only be moved to a target module that does not already define anotherstruct.

    These side conditions are based on definitions written by László Löveiet al. in this paper:[1]

▲ back to Index


Add or remove a parameter

  • Category: Traditional Refactorings.

  • Motivation: This refactoring is used when it is necessary to request additional information from the callers of a function or the opposite situation, when some information passed by the callers is no longer necessary. The transformation promoted by this refactoring usually creates a new function with the same name as the original, but with a new parameter added or a parameter removed, and the body of the original function is replaced by a call to the new function, subsequently replacing the calls to the original function with calls to the new function. Thanks to the possibility of specifying default values for function parameters in Elixir, using the\\ operator, we can simplify the mechanics of this refactoring, as shown in the following example.

  • Examples: The following code has afoo/1 function that always sum the constant +1 to thevalue passed as a parameter.

    # Before refactoring:deffoo(value)dovalue+1end

    We want to add a parameter to the functionfoo/1 to generalize the constant used in the sum. To do this, we can addnew_arg at the end of the parameter list, accompanied by the default value\\ 1. In addition, we should modify the body of the function to use this new parameter, as shown below.

    # After refactoring:deffoo(value,new_arg\\1)dovalue+new_argend

    Note that although we have now only explicitly implemented thefoo/2 function, in Elixir this definition generates two functions with the same name, but with different arities:foo/1 andfoo/2. This will allow the callers of the original function to continue functioning without any changes. Although the example has only emphasized the addition of new parameters using default values, this feature can also be useful when we want to remove a parameter from a function, decreasing its arity. We can define a default value for the parameter to be removed when it is no longer used in the body of the function. This will keep the higher arity function callers working, even if providing an unused additional value. Additionally, new callers of the lower arity function can coexist in the codebase. When all old callers of the higher arity function are replaced by calls to the lower arity function, the parameter with the default value can finally be removed from the function without compromising any caller.

  • Side-conditions:

    • When adding a new parameter, its name (e.g.,new_arg) must not conflict with the name of any other parameter or local variable in the refactored function;

    • When adding a new parameter to a function and thus increasing its arity, this operation must not conflict with functions of the same name and arity that are already defined or imported into the refactored module;

    • When removing a parameter, it should not be used in the definition of the function that is being refactored;

    • When removing a parameter from a function and thus reducing its arity, this operation must not conflict with functions of the same name and arity that are already defined or imported into the refactored module.

    These side conditions are based on definitions provided by members of the HaRe project (Haskell Refactorer tool), as described in the following link:[1]

▲ back to Index


Grouping parameters in tuple

  • Category: Traditional Refactorings.

  • Motivation: This refactoring can be useful to eliminate theLong Parameter List code smell. When functions have very long parameter lists, their interfaces can become confusing, making the code difficult to read and increasing the propensity for bugs. This refactoring concentrates on grouping a number of a function's sequential and related parameters into atuple, thereby shortening the list of parameters. The function`s callers are also modified by this refactoring to correspond to the new parameter list.Tuple is a data type supported by Elixir and is often used to group a fixed number of elements.

  • Examples: The following code presents theFoo module, composed only by therand/2 function. This function takes two values as a parameter and returns the random number present in the range defined by the two parameters. Althoughrand/2's parameter list is not necessarily long, try to imagine a scenario where a function has a list consisting of five or more parameters, for example. Furthermore, not always all parameters of a function can be grouped as in this example.

    # Before refactoring:defmoduleFoododefrand(first,last)doEnum.random(first..last)endend#...Use examples...iex(1)>Foo.rand(1,9)4iex(2)>Foo.rand(2,8)2

    We want to find and group parameters that are related, thus decreasing the size of the list. Note that in this case, the two parameters in the list form an interval, so they are related and can be grouped to compose the functionrand/1, as shown below.

    # After refactoring:defmoduleFoododefrand({first,last}=group)doEnum.random(first..last)endend#...Use examples...iex(1)>g={1,9}#<= tuple definitioniex(2)>Foo.rand(g)5iex(3)>g={2,8}#<= tuple definitioniex(4)>Foo.rand(g)7iex(5)>g={2,8,3}#<= wrong tuple definition!iex(6)>Foo.rand(g)**(FunctionClauseError) no function clause matchinginFoo.rand/1

    The functionrand/1 performs pattern matching with the value of its single parameter. This, in addition to allowing the extraction of the values that make up thetuple, allows for validating whether the format of the parameter received in the call is really that of thetuple of the expected length. Also, note that this refactoring updates all function callers to the new parameter list.

    Important: Although this refactoring has grouped parameters usingtuples, we can find in different functions identical groups of parameters that could be grouped (i.e., Data Clumps). In that case, is better to create astruct to group these parameters and reuse thisstruct to refactor all functions where this group of parameters occurs.

  • Side-conditions:

    • The grouped parameters must be sequential in the original parameter list of the modified function;

    • The function created by this refactoring, with reduced arity, must not conflict with any existing functions defined in the same module or imported by it;

    • The refactored function must not be an OTP callback function (e.g.,GenServer.handle_call/3).

    These side conditions are based on definitions written by Huiqing Liet al. in this paper:[1]

▲ back to Index


Reorder parameter

  • Category: Traditional Refactoring.

  • Motivation: Although the order of parameters does not change the complexity of executing a code for the machine, when a function has parameters defined in an order that does not group similar semantic concepts, the code can become more confusing for programmers, making it difficult to read and also becoming more prone to errors during its use. When we find functions with poorly organized parameters, we must reorder them in a way that allows for better readability and meaning for programmers.

  • Examples: The following code illustrates this refactoring. Before the refactoring, we have a functionarea/3, responsible for calculating the area of a trapezoid. Although the body of this function is correct, the two bases of the trapezoid are not sequential parameters, so this can confuse a programmer when this function is called. The area of a trapezoid that hasmajor_base = 24 cm,minor_base = 9 cm, andheight = 15 cm equals 247.5 cm^2. In the first call ofarea/3 in the example, the programmer imagined that the values of the bases would be the first two parameters of the function and thus had a calculation error that could easily go unnoticed.

    # Before refactoring:defarea(major_base,height,minor_base)do((major_base+minor_base)*height)/2end#...Use examples...iex(1)>Trapezoid.area(24,9,15)#<- misuse175.5iex(2)>Trapezoid.area(24,15,9)247.5

    We want to reorder the parameters ofarea/3 to make them semantically organized. To do so, we should create a new functionnew_area/3, which will have the parameters reordered and copy the body of thearea/3 to it. Additionally, the body of thearea/3 should be replaced by a call to thenew_area/3:

    # After refactoring:defnew_area(major_base,minor_base,height)do#<-- can be renamed in the future!((major_base+minor_base)*height)/2enddefarea(major_base,height,minor_base)do#<-- must be deleted in the future!new_area(major_base,minor_base,height)end#...Use examples...iex(1)>Trapezoid.new_area(24,9,15)247.5iex(2)>Trapezoid.area(24,15,9)247.5
  • Side-conditions:

    • The new function created by this refactoring must have the same arity as the original function and have a name different from all others defined or imported by the refactored module, avoiding conflicts;
    • Immediately after the refactoring, there should be no calls to the newly created function (e.g.,new_area/3) anywhere other than within the body of the original function (e.g.,area/3).

▲ back to Index


Extract function

  • Category: Traditional Refactoring.

  • Motivation: For us to have code with easy readability, it is important that its purpose be clearly exposed, not requiring a developer to spend too much time to understand its purpose. Sometimes we come across functions that concentrate on many purposes and therefore become long (Long Functions), making their maintenance difficult. It is common in such functions to find code comments used to explain the purpose of a sequence of lines. Whenever we encounter functions with these characteristics, we should extract these code sequences into a new function that has a name that clearly explains its purpose. In the original function, the extracted code block should be replaced by a call to the new function. This refactoring makes functions smaller and more readable, thus facilitating their maintenance.

  • Examples: The following code illustrates this refactoring. Before the refactoring, we have a functionticket_booking/5, responsible for booking an airline ticket for a passenger. All the main steps of the booking are done through a sequence of operations chained by pipe operators. After payment confirmation, the booking process is finalized by returning a tuple containing important reservation data that must be informed to the passenger. We can observe that the last 3 lines of theticket_booking/5 function are responsible for presenting a report. Note that these lines were preceded by a comment attempting to explain their purposes, highlighting that these expressions are misplaced withinticket_booking/5 and even require documentation to help understand the code.

    # Before refactoring:defticket_booking(passenger,air_line,date,credit_card,seat)do{company,contact_info,cancel_policy}=check_availability(air_line,date)|>documents_validation(passenger)|>select_seat(seat)|>payment(credit_card)#print booking reportIO.puts("Booking made at the company:#{company}")IO.puts("Any doubt, contact:#{contact_info}")IO.puts("For cancellations, see company policies:#{cancel_policy}")end

    We want to make this code more concise, reducing the size ofticket_booking/5 and improving its readability. To achieve this, we should create a new functionreport/1 that will receive a tuple as a parameter, extract the values from the tuple to variables via pattern matching, and finally present the report containing these values. In addition, the body ofticket_booking/5 should be updated to include a call toreport/1 to replace the extracted lines of code.

    # After refactoring:defreport({company,contact_info,cancel_policy}=confirmation)doIO.puts("Booking made at the company:#{company}")IO.puts("Any doubt, contact:#{contact_info}")IO.puts("For cancellations, see company policies:#{cancel_policy}")enddefticket_booking(passenger,air_line,date,credit_card,seat)docheck_availability(air_line,date)|>documents_validation(passenger)|>select_seat(seat)|>payment(credit_card)|>report()#<- extracted function call!end

    This refactoring not only improves the readability ofticket_booking/5, but also enables more code reuse, sincereport/1 may eventually be called by other functions in the codebase.

  • Side-conditions:

    • The name and arity of extracted function created in this refactoring (e.g.,report/1) must not conflict with the name of any other function already defined or imported by the refactored module;

    • The extracted sequence of expressions must not make recursive calls to the original function;

    • The extracted sequence of expressions must not be originally included in aguard sequence;

    • The extracted sequence of expressions must not be originally part of a pattern;

    • The extracted sequence of expressions must not be originally within amacro definition.

    These side conditions are based on definitions provided by members of the RefactorErl project, as described in the following link:[1]

▲ back to Index


Inline function

  • Category: Traditional Refactoring.

  • Motivation: This refactoring is the inverse ofExtract Function. We typically extract functions to reduce their size, making them more readable and easier to maintain. However, in some situations, the body of a function basically just delegates to the call of another function. In these cases, the purpose of the function body is as clear as its name, and there is no benefit in keeping the declaration of this function. In fact, excessive delegation may create code with very indirect execution flows that are difficult to debug. In these situations, an inline function can be used, replacing all calls to the function with its body, and then getting rid of the function.

  • Examples: The following code illustrates this refactoring. Before the refactoring, we have a module calledOrder composed of the private functionsum_list/1 and the public functionget_amount/1. The functionget_amount/1 receives a list of items in an order and delegates the sum of all item values to thesum_list/1 function. As we can see, thesum_list/1 function simply calls theEnum.sum/1 function provided natively by Elixir, thus being an example of excessive and unnecessary delegation.

    # Before refactoring:defmoduleOrderdodefpsum_list(list)doEnum.sum(list)enddefget_amount(order_itens)dosum_list(order_itens)endend

    To eliminate the excessive delegation generated by thesum_list/1 function, we will replace all calls tosum_list/1 with its body. Then, we can delete thesum_list/1 function from theOrder module since it will no longer be necessary.

    # After refactoring:defmoduleOrderdodefget_amount(order_itens)doEnum.sum(order_itens)#<- inlined function!endend

    This refactoring preserves the behavior of the function and will make it easier for the programmer to debug the code.

  • Side-conditions:

    • The body of the original function, which is used to replace its calls, must not contain variables that conflict with the names of other variables already existing in the scope where the function calls occurred;

    • If the calls replaced by the body of a function refer to a function defined in a different module, the body of that function must not contain calls to functions that are not imported in the scope where the replaced calls occurred.

    These side conditions are based on definitions provided by members of the RefactorErl project, as described in the following link:[1]

▲ back to Index


Folding against a function definition

  • Category: Traditional Refactorings.

  • Motivation: This refactoring can be used in the context of removingDuplicated Code, replacing a set of expressions with a call to an existing function that performs the same processing as the duplicated code. The opportunity to apply this refactoring may occur after the chained execution of theExtract function andGeneralise a function definition refactorings. After generalizing a function that has been previously extracted, it is possible that there may still be some code snippets in the codebase that are duplicated with the generalized function. This refactoring aims to, from a source function, find code that is duplicated in relation to it and replace the duplications with calls to the source function. Some adaptations in these new call points to the source function may be necessary to preserve the code's behavior.

  • Examples: The following code exemplifies this refactoring. Before the refactoring, we have aClass module composed of two functions. The functionreport/1 was previously extracted from a code snippet not shown in this example. Later, this extracted function was generalized, resulting in its current format. The functionimprove_grades/3 already existed in theClass module beforereport/1 was generated through refactoring. Note thatimprove_grades/3 has code snippets that are duplicated withreport/1.

    # Before refactoring:defmoduleClassdodefstruct[:id,:grades,:avg,:worst,:best]defreport(list)do#<- Generated after extraction and generalisation!avg=Enum.sum(list)/length(list){min,max}=Enum.min_max(list){avg,min,max}enddefimprove_grades(class_id,grades,students_amount)dohigh_grade=Enum.max(grades)adjustment_factor=100/high_gradenew_grades=Enum.map(grades,&(&1*adjustment_factor)|>Float.round(2))grades_avg=Enum.sum(new_grades)/students_amount#<- duplicated code{w,b}=Enum.min_max(new_grades)#<- duplicated code%Class{id:class_id,grades:new_grades,avg:grades_avg,worst:w,best:b}endend#...Use examples...iex(1)>Class.improve_grades(:software_engineering,[26,49,70,85,20,75,74,15],8)%Class{id::software_engineering,grades:[30.59,57.65,82.35,100.0,23.53,88.24,87.06,17.65],avg:60.88375,worst:17.65,best:100.0}

    We want to eliminate the duplicated code inimprove_grades/3. To achieve this, we can replace the duplicated code snippet with a call toreport/1. Note that some adaptations to the function call that will replace the duplicated code may be necessary.

    # After refactoring:defmoduleClassdodefstruct[:id,:grades,:avg,:worst,:best]defreport(list)doavg=Enum.sum(list)/length(list){min,max}=Enum.min_max(list){avg,min,max}enddefimprove_grades(class_id,grades,students_amount)dohigh_grade=Enum.max(grades)adjustment_factor=100/high_gradenew_grades=Enum.map(grades,&(&1*adjustment_factor)|>Float.round(2)){grades_avg,w,b}=report(new_grades)#<- Folding against a function definition!%Class{id:class_id,grades:new_grades,avg:grades_avg,worst:w,best:b}endend#...Use examples...iex(1)>Class.improve_grades(:software_engineering,[26,49,70,85,20,75,74,15],8)%Class{id::software_engineering,grades:[30.59,57.65,82.35,100.0,23.53,88.24,87.06,17.65],avg:60.88375,worst:17.65,best:100.0}

    Also note that in this example, after the refactoring is done, the third parameter of theimprove_grades/3 function is no longer used in the function body. This is an opportunity to apply theAdd or remove parameter refactoring.

  • Side-conditions:

    • The duplicated code to be replaced and the function to be called to perform this refactoring must be defined in the same module;

    • After replacing the duplicated code with a function call, compensations (e.g., pattern matching to extract values returned by the function) may be necessary to maintain the code's original behavior.

    These side conditions are based on definitions provided by members of the HaRe project (Haskell Refactorer tool), as described in the following link:[1]

▲ back to Index


Extract constant

  • Category: Traditional Refactorings.

  • Motivation: This refactoring aims to improve code readability and maintainability. When we use meaningless numbers (magic numbers) directly in expressions, code comprehension can become more complex for humans. Additionally, if the same meaningless number is scattered throughout the codebase and needs to be modified in the future, it can generate a significant maintenance burden, increasing the risk of bugs. To improve the code, this refactoring seeks to create a constant with a meaningful name for humans and replace occurrences of the meaningless number with the extracted constant.

  • Examples: The following code provides an example of this refactoring. Prior to the refactoring, we had aCircle module consisting of two functions. Both functions used the magic number3.14. Although this example contains simple code that may not seem to justify the developer's concern with extracting constants, try to imagine a more complex scenario involving larger and non-trivial code that makes more extensive use of these meaningless numbers. This could cause a lot of headache for a developer.

    # Before refactoring:defmoduleCircledodefarea(r)do3.14*r**2enddefcircumference(r)do2*3.14*rendend#...Use examples...iex(1)>Circle.area(3)28.26iex(2)>Circle.circumference(3)18.84

    To improve the comprehension of this code and make it easier to maintain, we can create amodule attribute with a human-readable name (@pi) and replace the numbers with the use of this attribute.

    # After refactoring:defmoduleCircledo@pi3.14#<- extracted constant!defarea(r)do@pi*r**2enddefcircumference(r)do2*@pi*rendend#...Use examples...iex(1)>Circle.area(3)28.26iex(2)>Circle.circumference(3)18.84

    This not only gives meaning to the number but also facilitates maintenance in case it needs to be changed. In the case of@pi, if we wish to improve the precision of the calculations by adding more decimal places to its value, this can be easily done in the refactored code.

▲ back to Index


Temporary variable elimination

  • Category: Traditional Refactorings.

  • Motivation: This is a refactoring motivated by the compiler optimization technique known as copy propagation. Copy propagation is a transformation that, for an assignment of the forma =b, replaces uses of the variablea with the value of the variableb, thus eliminating redundant computations. This refactoring can be very useful for eliminating temporary variables that are responsible only for storing results to be returned by a function, or even intermediate values used during processing.

  • Examples: The following code provides an example of this refactoring. Prior to the refactoring, we have a functionbar/2 that takes an integer valueb and alist as parameters. The function returns a tuple containing two elements, the first of which is the value contained in the indexc of thelist and the second of which is the sum of all elements in a modified version of thelist.

    # Before refactoring:defmoduleFoododefbar(b,list)whenis_integer(b)doa=b*2c=aresult_1=Enum.at(list,c)r=aresult_2=Enum.map(list,&(&1+r))|>Enum.sum(){result_1,result_2}endend#...Use examples...iex(1)>Foo.bar(2,[1,2,3,4,5,6]){5,45}

    To perform this processing, the code above makes excessive and unnecessary use of temporary variables. As shown below, after the refactoring, these temporary variables will be replaced by their values and subsequently removed when they are no longer used in any location.

    # After refactoring:defmoduleFoododefbar(b,list)whenis_integer(b)do{Enum.at(list,b*2),Enum.map(list,&(&1+b*2))|>Enum.sum()}endend#...Use examples...iex(1)>Foo.bar(2,[1,2,3,4,5,6]){5,45}

    This refactoring can promote a significant simplification of some code, as well as avoid redundant computations that can harm performance.

  • Side-conditions:

    • The variable has only one binding occurrence on the left-hand side of a pattern matching expression and is not part of a compound pattern;

    • The expression bound to the variable is pure, meaning it has no side effects.

    These side conditions are based on definitions provided by members of the RefactorErl project, as described in the following link:[1]

▲ back to Index


Extract expressions

  • Category: Traditional Refactorings.

  • Note: Formerly known as "Merge expressions".

  • Motivation: This refactoring, in a way, behaves as the inverse ofTemporary variable elimination. When programming, we may sometimes come across unavoidably large and hard-to-understand expressions. With this refactoring, we can break down those expressions into smaller parts and assign them to local variables with meaningful names, thus facilitating the overall understanding of the code. In addition, this refactoring can help eliminateDuplicated Code, as the variables extracted from the expressions can be reused in various parts of the codebase, avoiding the need for repetition of long expressions.

  • Examples: The following code provides an example of this refactoring. Prior to the refactoring, we have a moduleBhaskara composed of the functionsolve/3, responsible for finding the roots of a quadratic equation. This function returns a tuple with the roots or their non-existence.

    # Before refactoring:defmoduleBhaskaradodefsolve(a,b,c)doifb*b-4*a*c<0do{:error,"No real roots"}elsex1=(-b+(b*b-4*a*c)**0.5)/(2*a)x2=(-b-(b*b-4*a*c)**0.5)/(2*a){:ok,{x1,x2}}endendend#...Use examples...iex(1)>Bhaskara.solve(1,3,-4){:ok,{1.0,-4.0}}iex(2)>Bhaskara.solve(1,2,1){:ok,{-1.0,-1.0}}iex(3)>Bhaskara.solve(1,2,3){:error,"No real roots"}

    Note that in this function, besides the expressionb*b - 4*a*c being repeated several times, including within a larger expression, the lack of meaning forb*b - 4*a*c can make the code less readable. We can solve this by extracting a new variabledelta, assigningb*b - 4*a*c to this new variable, and replacing all instances of this expression with the use of thedelta variable.

    # After refactoring:defmoduleBhaskaradodefsolve(a,b,c)dodelta=(b*b-4*a*c)#<- extracted variable!ifdelta<0do{:error,"No real roots"}elsex1=(-b+delta**0.5)/(2*a)x2=(-b-delta**0.5)/(2*a){:ok,{x1,x2}}endendend#...Use examples...iex(1)>Bhaskara.solve(1,3,-4){:ok,{1.0,-4.0}}iex(2)>Bhaskara.solve(1,2,1){:ok,{-1.0,-1.0}}iex(3)>Bhaskara.solve(1,2,3){:error,"No real roots"}
  • Side-conditions:

    • The name of the new variable created in this refactoring (e.g.,delta) must not conflict with the name of any other variable already defined in the same scope;

    • The expression to be extracted into a variable must originally be contained within the body of a function and not be part of a guard expression;

    • The expression cannot be replaced by a variable if any of its sub-expressions are not pure, meaning they have side effects.

    These side conditions are based on definitions provided by members of the RefactorErl project, as described in the following link:[1]

    Recalling previous refactorings: Although the refactored code shown above has made the code more readable, it still has opportunities for applying other refactorings previously documented in this catalog. Note that for the calculation of the roots, we have two lines of code that are practically identical. In addition, we have two temporary variables (x1 andx2) that have only the purpose of storing results that will be returned by the function. If we take this refactored version of the code after applyingExtract expressions and apply a composite refactoring with the sequence ofExtract function ->Generalise a function definition ->Fold against a function definition ->Temporary variable elimination, we can arrive at the following version of the code:

    # After a composite refactoring:defmoduleBhaskaradodefproot(a,b,delta,operation)dooperation.(-b,delta**0.5)/(2*a)enddefsolve(a,b,c)dodelta=(b*b-4*a*c)ifdelta<0do{:error,"No real roots"}else{:ok,{root(a,b,delta,&Kernel.+/2),root(a,b,delta,&Kernel.-/2)}}endendend#...Use examples...iex(1)>Bhaskara.solve(1,3,-4){:ok,{1.0,-4.0}}iex(2)>Bhaskara.solve(1,2,1){:ok,{-1.0,-1.0}}iex(3)>Bhaskara.solve(1,2,3){:error,"No real roots"}

▲ back to Index


Splitting a large module

  • Category: Traditional Refactorings.

  • Motivation: This refactoring can be used as a solution for removing the code smellLarge Module, also known as Large Class in object-oriented languages. When a module in Elixir code does the work of two or more, it becomes large, poorly cohesive, and difficult to maintain. In these cases, we should split this module into several new ones, moving to each new module only the attributes and functions with purposes related to their respective goals.

  • Examples: The code example below demonstrates the application of this refactoring technique. In this case, theShoppingCart module is excessively large and lacks cohesion, as it combines functions related to at least four distinct and unrelated business rules.

    # Before refactoring:defmoduleShoppingCartdo# Rule 1defcalculate_total(items,subscription)do# ...end# Rule 1defcalculate_shipping(zip_code,%{id:3}),do:0.0defcalculate_shipping(zip_code,%{id:4}),do:0.0defcalculate_shipping(zip_code,_),do10.0*Location.calculate(zip_code)end# Rule 2defapply_discount(total,%{id:3}),do:total*0.9defapply_discount(total,%{id:4}),do:total*0.9defapply_discount(total,_),do:total# Rule 3defsend_message_subscription(%{id:3},_),do:nildefsend_message_subscription(%{id:4},_),do:nildefsend_message_subscription(subscription,user),do:# something# Rule 4defreport(user,order)do# ...endend

    Applying this refactoring three times,ShoppingCart can be splitted, and some of its functions could be moved to other new modules (Item,Subscription, andUtil), thus increasing the codebase overall cohesion.

    # After refactoring:defmoduleShoppingCartdo# Rule 1defcalculate_total(items,subscription)do# ...end# Rule 1defcalculate_shipping(zip_code,%{id:3}),do:0.0defcalculate_shipping(zip_code,%{id:4}),do:0.0defcalculate_shipping(zip_code,_),do10.0*Location.calculate(zip_code)endend
    # After refactoring:defmoduleItemdo# Rule 2defapply_discount(total,%{id:3}),do:total*0.9defapply_discount(total,%{id:4}),do:total*0.9defapply_discount(total,_),do:totalend
    # After refactoring:defmoduleSubscriptiondo# Rule 3defsend_message_subscription(%{id:3},_),do:nildefsend_message_subscription(%{id:4},_),do:nildefsend_message_subscription(subscription,user),do:# somethingend
    # After refactoring:defmoduleUtildo# Rule 4defreport(user,order)do# ...endend

    Each application of this refactoring involves creating a new module, selecting the set of definitions that should be moved to that new module, and applying theMoving a definition refactoring to each of those selected definitions. Any reference and dependency issues inherent in moving functions between modules are compensated for by theMoving a definition refactoring. Although this does not happen in the above example, in some cases, after splitting an original module into several smaller and more cohesive modules, the name of the original module can no longer makes sense, providing an opportunity to also apply theRename an identifier refactoring.

▲ back to Index


Remove nested conditional statements in function calls

  • Category: Traditional Refactoring.

  • Note: Formerly known as "Simplifying nested conditional statements".

  • Motivation: Sometimes nested conditional statements can unnecessarily decrease the readability of the code. This refactoring aims to simplify the code by eliminating unnecessary nested conditional statements.

  • Examples: The following code shows an example of this refactoring. Before the refactoring, we have the functionsconvert/2 andqux/3. The private functionconvert/2 takes alist and a boolean valueswitch as parameters. Ifswitch is true, thelist is converted to a tuple; otherwise, thelist is not modified. The public functionqux/3 takes alist, avalue, and anindex as parameters and then calls theconvert/2 function. If thelist contains thevalue at theindex,qux/3 calls theconvert/2 function with the second parameter set to true; otherwise, the second parameter is set to false.

    # Before refactoring:defmoduleFoododefpconvert(list,switch)docaseswitchdotrue->{:tuple,List.to_tuple(list)}_->{:list,list}endenddefqux(list,value,index)docaseconvert(list,caseEnum.at(list,index)do^value->true_->falseend)do{:tuple,_}->"Something..."{:list,_}->"Something else..."endendend#...Use examples...iex(1)>Foo.qux([1,7,3,8],7,0)"Something else..."iex(2)>Foo.qux([1,7,3,8],7,1)"Something..."

    Note that the functionqux/3 uses two nestedcase statements to perform its operations, with the innermostcase statement responsible for setting the boolean value of the second parameter in the call toconvert/2. As shown in the following code, we can simplify this code by replacing the innermostcase statement with a strict equality comparison (===).

    # After refactoring:defmoduleFoodo...defqux(list,value,index)docaseconvert(list,Enum.at(list,index)===value)do{:tuple,_}->"Something..."{:list,_}->"Something else..."endendend#...Use examples...iex(1)>Foo.qux([1,7,3,8],7,0)"Something else..."iex(2)>Foo.qux([1,7,3,8],7,1)"Something..."

▲ back to Index


Move file

  • Category: Traditional Refactoring.

  • Motivation: Move a project file between directories, which contains code such as modules, macros, structs, etc. This refactoring can improve the organization of an Elixir project, grouping related files in the same directory, which may, for example, belong to the same architectural layer. This refactoring can also impact the updating of references made by dependents of the code present in the moved file.

▲ back to Index


Remove dead code

  • Category: Traditional Refactoring.

  • Motivation: Dead code (i.e., unused code) can pollute a codebase making it longer and harder to maintain. This refactoring aims to clean the codebase by eliminating code definitions that are not being used.

  • Examples: The following code shows an example of this refactoring. Before the refactoring, we have a functionbar/2 that modifies the two values received as parameters and then returns the power of both.

    # Before refactoring:defmoduleFoododefbar(v1,v2)dov1=v1**2v2=v2+5dead_code=v2/2#<= can be removed!{:ok,v1**v2}endend#...Use examples...iex(1)>c("sample.ex")warning:variable"dead_code"isunused...sample.ex:5: Foo.bar/2iex(2)>Foo.bar(2,1){:ok,4096}

    Note that when this code is compiled, Elixir's compiler itself informs the existence of unused code that could be eliminated to clean up the codebase. As shown in the following code, this refactoring eliminated thedead_code inbar/2 without causing any side effects to the function's behavior.

    # After refactoring:defmoduleFoododefbar(v1,v2)dov1=v1**2v2=v2+5{:ok,v1**v2}endend#...Use examples...iex(1)>Foo.bar(2,1){:ok,4096}
  • Side-conditions:

    • Considering that the code removed by this refactoring must be either commented out or not referenced by any other part of the codebase, this refactoring can be performed without the need to meet any additional specific conditions.

▲ back to Index


Introduce a temporary duplicate definition

  • Category: Traditional Refactoring.

  • Note: Formerly known as "Introduce or remove a duplicate definition".

  • Motivation: When we want to test a modification in a code without losing its original definition, we can temporarily duplicate it with a new identifier. Once this new version of the code is approved, it will replace the original version and the duplication will be removed.

  • Examples: The following code shows an example of this refactoring. Before the refactoring, we have a functionbar/2 that returns the power of their parameters.

    # Before refactoring:defmoduleFoododefbar(v1,v2)dov3=v1**v2{:ok,v3}endend#...Use examples...iex(1)>Foo.bar(5,2){:ok,25}

    Imagine that for some reason it is necessary to change the definition ofv3, but while the new version ofv3 is not approved, we also want to keep the original version in the codebase. The following code shows the application of this refactoring, duplicating the definition ofv3.

    # After refactoring:defmoduleFoododefbar(v1,v2)dov3=v1**v2#<= definition to be changed!v3_duplication=v1**v2#<= original version!{:ok,v3}endend#...Use examples...iex(1)>Foo.bar(5,2){:ok,25}
  • Side-conditions:

    • Note that the identifier of the introduced duplicated code (v3_duplication) should not conflict with any other existing identifier in the code;

    • Once the new version of the code has been implemented and approved, the duplication can be removed from the codebase by this refactoring. If the new version is disapproved, we can return to the original version by applyingRename an identifier to it.

    These side conditions are based on definitions provided by members of the HaRe project (Haskell Refactorer tool), as described in the following link:[1]

▲ back to Index


Introduce overloading

  • Category: Traditional Refactoring.

  • Motivation: Function overloading enables the creation of variations of an existing function, that is, the definition of two or more functions with identical names but different parameters. In Elixir, this can be done with functions of different arities or with multi-clause functions of the same arity. This refactoring allows for the creation of a variation of a function, enabling its use in different contexts.

  • Examples: The following code shows an example of this refactoring. Before the refactoring, we have a functiondiscount/1 that allows applying a 30% discount on orders that cost at least100.0.

    # Before refactoring:defmoduleOrderdodefstruct[date:nil,total:nil]defdiscount(%Order{total:t}=s)whent>=100.0do%Order{s|total:t*0.7}endend#...Use examples...iex(1)>Order.discount(%Order{total:150.0,date:~D[2022-11-10]})%Order{date:~D[2022-11-10],total:105.0}iex(2)>Order.discount(%Order{total:90.0,date:~D[2022-10-18]})**(FunctionClauseError) no function clause matchinginOrder.discount/1

    Consider a scenario where this e-commerce wants to implement new discount rules for new situations or specific dates. This could be done by overloading thediscount/1 function, creating for example a new clause for it that will be applied on purchases made on Christmas day, and also a variationdiscount/2, that could be applied for discounts on exceptional cases. The following code presents these two refactorings of the originaldiscount/1 function.

    # After refactoring:defmoduleOrderdodefstruct[date:nil,total:nil]# newdefdiscount(%Order{date:d,total:t}=s)whend.day==25andd.month==12do%Order{s|total:t*0.1}end# originaldefdiscount(%Order{total:t}=s)whent>=100.0do%Order{s|total:t*0.7}end# newdefdiscount(%Order{total:t}=s,value)do%Order{s|total:t*value}endend#...Use examples...iex(1)>Order.discount(%Order{total:150.0,date:~D[2022-12-25]})%Order{date:~D[2022-12-25],total:15.0}iex(2)>Order.discount(%Order{total:150.0,date:~D[2022-11-10]})%Order{date:~D[2022-11-10],total:105.0}iex(3)>Order.discount(%Order{total:90.0,date:~D[2022-10-18]},0.8)%Order{date:~D[2022-10-18],total:72.0}

▲ back to Index


Remove import attributes

  • Category: Traditional Refactoring.

  • Motivation: The use of theimport directive in a module allows calling functions defined in other modules without having to specify them directly in each call. While this can reduce the size of code, the use ofimport can also harm the readability of code, making it difficult to identify directly the source of a function being called. This refactoring allows removing theimport directives in a module, replacing all calls to imported functions with the formatModule.function(args).

  • Examples: The following code shows an example of this refactoring.

    # Before refactoring:defmoduleBardodefsum(v1,v2)dov1+v2endenddefmoduleFoodoimportBar#<= to be removed!defqux(value_1,value_2)dosum(value_1,value_2)#<= imported function!endend#...Use examples...iex(1)>Foo.qux(1,2)3
    # After refactoring:defmoduleBardodefsum(v1,v2)dov1+v2endenddefmoduleFoododefqux(value_1,value_2)doBar.sum(value_1,value_2)#<= calling with a fully-qualified nameendend#...Use examples...iex(1)>Foo.qux(1,2)3
  • Side-conditions:

    • This refactoring is free from any preconditions or postconditions. Considering that after this refactoring, the functions that were originally imported will be called with a fully-qualified name, even if the module that acted as the importer defines a function with the same name/arity as a function that was imported before the refactoring, no conflict will occur.

▲ back to Index


Introduce import

  • Category: Traditional Refactorings.

  • Motivation: This refactoring is the inverse ofRemove import attributes. Recall that Remove import attributes allows you to removeimport directives from a module, replacing all calls to imported functions with fully-qualified name calls (Module.function(args)). In contrast, Introduce import focuses on replacing fully-qualified name calls of functions from other modules with calls that use only the name of the imported functions.

  • Examples: To better understand, take a look at the example inRemove import attributes in reverse order, that is,# After refactoring: -># Before refactoring:.

  • Side-conditions:

    • When a moduleFoo is importing functions from a moduleBar, the moduleFoo must not have any previously defined functions with the same names/arity as the functions imported fromBar, thus avoiding conflicts;

    • Additionally, the moduleFoo must not have previously imported functions defined in other modules that have the same name/arity as the functions imported fromBar.

    This side condition is based on definitions provided by members of the RefactorErl project, as described in the following link:[1]

▲ back to Index


Group Case Branches

  • Category: Traditional Refactoring.

  • Motivation: The divide-and-conquer pattern refers to a computation in which a problem is recursively divided into independent subproblems, and then the subproblems' solutions are combined to obtain the solution of the original problem. Such a computation pattern can be easily parallelized because we can work on the subproblems independently and in parallel. This refactoring aims to restructure functions that utilize the divide-and-conquer pattern, making parallelization easier. Specifically, this refactoring allows us to partition the branches of acase statement in a divide-and-conquer function into two categories:(1) the base cases, and(2) the recursive cases. This restructuring replaces the originalcase statement with fourcase statements.

  • Examples: The following code examples illustrate a refactoring of the merge sort algorithm. Prior to the refactoring, themerge_sort/1 function had only a singlecase statement to handle both the base case and the recursive case.

    # Before refactoring:defmoduleFoododefmerge_sort(list)docaselistdo[]->[][h]->[h]_->half=length(list)|>div(2)right=merge_sort(Enum.take(list,half))left=merge_sort(Enum.drop(list,half))merge(right,left)endendend#...Use examples...iex(1)>Foo.merge_sort([3,20,9,2,7,99,80,30])[2,3,7,9,20,30,80,99]

    After refactoring, thiscase statement is replaced by four separatecase statements, each with its respective role:

    • (1 and 2) Determine whether the instance is a base case (true) or a recursive case (false);

    • (3 and 4) Define which specific base case or recursive case we are dealing with, respectively.

    # After refactoring:defmoduleFoododefmerge_sort(list)dois_base=caselistdo#<= role 1[]->true[_h]->true_->falseendcaseis_basedo#<= role 2true->caselistdo#<= role 3[]->[][h]->[h]endfalse->caselistdo#<= role 4_->half=length(list)|>div(2)right=merge_sort(Enum.take(list,half))left=merge_sort(Enum.drop(list,half))merge(right,left)endendendend#...Use examples...iex(1)>Foo.merge_sort([3,20,9,2,7,99,80,30])[2,3,7,9,20,30,80,99]
  • Side-conditions:

    • The head expression of the originalcase statement (e.g.,case list do) must be pure before refactoring. In other words, this expression should not contain side effects, such as I/O operations, persist data in database, update GUI state, sending/receiving messages, etc.

    This side condition is based on definitions written by Tamás Kozsiket al. in this paper:[1]

▲ back to Index


Move expression out of case

  • Category: Traditional Refactoring.

  • Motivation: The divide-and-conquer pattern refers to a computation in which a problem is recursively divided into independent subproblems, and then the subproblems' solutions are combined to obtain the solution of the original problem. Such a computation pattern can be easily parallelized because we can work on the subproblems independently and in parallel. This refactoring aims to restructure functions that utilize the divide-and-conquer pattern, making parallelization easier. More precisely, this refactoring moves an expression outside of acase statement when it is repeated at the end of all branches.

  • Examples: The following code examples demonstrate this refactoring. Before the refactoring, thebar/2 function has acase statement with two branches. At the end of both branches, the expressionInteger.is_odd(value) is repeated.

    # Before refactoring:defmoduleFoododefbar(confirm,list)docaseconfirmdotrue->value=Enum.at(list,0)Integer.is_odd(value)false->value=Enum.at(list,length(list)-1)Integer.is_odd(value)endendend#...Use examples...iex(1)>Foo.bar(true,[6,5,4,3,2,1])falseiex(2)>Foo.bar(false,[6,5,4,3,2,1])true

    After the refactoring,bar/2 retains the same behavior, but now with the expressionInteger.is_odd(value) moved outside thecase statement.

    # After refactoring:defmoduleFoododefbar(confirm,list)dovalue=caseconfirmdotrue->Enum.at(list,0)false->Enum.at(list,length(list)-1)endInteger.is_odd(value)#<= out of case!endend#...Use examples...iex(1)>Foo.bar(true,[6,5,4,3,2,1])falseiex(2)>Foo.bar(false,[6,5,4,3,2,1])true
  • Side-conditions:

    • The expressions at the end of the originalcase branches should be lexically identical.

    This side condition is based on definitions written by Tamás Kozsiket al. in this paper:[1]

▲ back to Index


Simplifying checks by using truthness condition

  • Category: Traditional Refactorings.

  • Source: This refactoring emerged from a Grey Literature Review (GLR).

  • Motivation: When we know that a given data can have anil value and we need to return a default value if that data is indeednil, instead of usingis_nil/1 and anif-else block to test this condition and return the required value, we can utilize a short-circuit operator|| based on truthness conditions. This refactoring reduces the number of lines required for such an operation, maintaining clean and self-explanatory code.

  • Examples: In the following code, we use the built-in Elixir functionis_nil/1 to check if the value ofprice isnil and then return a default value if that is true. Otherwise, the original value ofprice is returned.

    # Before refactoring:defdefault(price)doifis_nil(price)do"$100"elsepriceendend

    We can refactor thedefault/1 function by simplifying the null check forprice, using only a test based on truthness conditions, as shown below.

    # After refactoring:defdefault(price)doprice||"$100"end

    In Elixir, the atomsnil andfalse are treated asfalsy values, whereas everything else is treated as atruthy value. When we use a short-circuit operator|| based on truthiness conditions, it returns the first expression that isn'tfalsy, thus the refactored code preserves the behavior of the original.

    This example is based on an original code by Malreddy Ankanna. Source:link

▲ back to Index


Reducing a boolean equality expression

  • Category: Traditional Refactorings.

  • Source: This refactoring emerged from a Grey Literature Review (GLR).

  • Motivation: When dealing with a boolean expression consisting of multiple equality comparisons involving the same variable and logicalor operators, we can reduce the number of lines of code and enhance readability by utilizing thein operator and a list containing all possible valid values for the variable. The advantages of this refactoring are particularly derived from the removal of partially duplicated code.

  • Examples: In the following code, we have a boolean expression that checks if anuser holds any of the four possible positions. If it is true for any of the positions, thedo_something/0 function is called. Otherwise, the code invokes thedo_something_else/0 function.

    # Before refactoring:ifuser=="admin"oruser=="super_admin"oruser=="agent"oruser=="super_agent"dodo_something()elsedo_something_else()end

    As shown next, we can refactor this code by reducing the size of the boolean expression in question and improving readability through the removal of duplicated code.

    # After refactoring:ifuserin["admin","super_admin","agent","super_agent"]dodo_something()elsedo_something_else()end

    This example is based on an original code by Malreddy Ankanna. Source:link

▲ back to Index


Transform "unless" with negated conditions into "if"

  • Category: Traditional Refactorings.

  • Source: This refactoring emerged from a Grey Literature Review (GLR).

  • Motivation: In Elixir, anunless statement is equivalent to anif with its condition negated. Therefore, while it is possible,unless statements should be avoided with a negated condition. The reason behind this is not technical but human-centric. Comprehending that a code block is executed only when a negated condition is not met is both confusing and challenging. Therefore, this refactoring aims to replaceunless statements with negated conditions withif statements, enhancing code readability.

  • Examples: In the following code, we have anunless statement that uses logical negation in its conditional test.

    # Before refactoring:unless!allowed?doproceed_as_planned()end

    This type of code, although simple, can easily confuse a developer and lead to errors. To improve readability and eliminate potential sources of confusion, we can refactor this code by removing the logical negation (!) from the conditional and replacing theunless statement with anif statement, thereby preserving the same behavior as the original code.

    # After refactoring:ifallowed?doproceed_as_planned()end

    These examples are based on code written in Credo's official documentation. Source:link

▲ back to Index


Replace conditional with polymorphism via Protocols

  • Category: Traditional Refactorings.

  • Source: This refactoring emerged from a Grey Literature Review (GLR).

  • Motivation: When dealing with a conditional statement that performs various actions based on data type or specific data properties, the code might become challenging to follow as the number of conditional possibilities increases. Additionally, if the same sequence of conditional statements, whether viaif..else,case, orcond, appears duplicated in the code, we may be forced to make changes in multiple parts of the code whenever a new check needs to be added to these duplicated sequences of conditional statements, characterizing the code smellSwitch Statements. This refactoring is essentially a translation of the traditional Fowler's refactoring, but usingprotocols, which are interfaces that can be implemented per data type in Elixir, introducing polymorphism to data structures and thus improving the code's extensibility to handle flow controls based on data types.

  • Examples: In the following code, we have a module calledBird, which defines astruct composed of three properties. When theplumage/1 function is called with a struct%Bird{} as a parameter, depending on the bird type and some other specific properties, its plumage is given a classification.

    # Before refactoring:defmoduleBirddodefstructtype:nil,number_of_coconuts:0,voltage:0defplumage(bird)docasebird.typedo"EuropeanSwallow"->"average""AfricanSwallow"->ifbird.number_of_coconuts>2do"tired"else"average"end"NorwegianParrot"->ifbird.voltage>100do"scorched"else"beautiful"endendendend#...Use examples...iex(1)>Bird.plumage(%Bird{type:"AfricanSwallow",number_of_coconuts:7})"tired"iex(2)>Bird.plumage(%Bird{type:"NorwegianParrot",voltage:7000})"scorched"iex(3)>Bird.plumage(%Bird{type:"EuropeanSwallow"})"average"

    Currently, this code classifies the plumage of only three distinct types of birds (EuropeanSwallow,AfricanSwallow, andNorwegianParrot). However, if new birds need to be classified in the future, this code may require significant changes, such as adding new properties to the%Bird{} struct definition and introducing additional conditional statements. If this same type of conditional logic repeats throughout the codebase, it may be necessary to make changes in many different places whenever a new bird type emerges, making the code less extensible, hard to maintain, and prone to errors.

    To address this complexity and improve the design of this code, we can initially transform theBird module into aprotocol with the same name, containing the interface for theplumage/1 function, as shown below.

    # After refactoring:defprotocolBirddodefplumage(bird)end

    Furthermore, each bird type should be transformed into its own module, defining its ownstruct and implementing theBirdprotocol, thus specializing theplumage/1 function for the peculiarities of each bird, as shown below.

    # After refactoring:defmoduleEuropeanSwallowdodefstructnumber_of_coconuts:0defimplBird,for:EuropeanSwallowdodefplumage(%EuropeanSwallow{}),do:"average"endenddefmoduleNorwegianParrotdodefstructvoltage:0defimplBird,for:NorwegianParrotdodefplumage(%NorwegianParrot{voltage:voltage})whenvoltage>100,do:"scorched"defplumage(_),do:"beautiful"endenddefmoduleAfricanSwallowdodefstructnumber_of_coconuts:0defimplBird,for:AfricanSwallowdodefplumage(%AfricanSwallow{number_of_coconuts:num})whennum>2,do:"tired"defplumage(_),do:"average"endend

    The calls to theplumage/1 function, which is now polymorphic, should be updated to receive specificstructs for each bird type instead of a generic%Bird{} parameter, as shown below.

    # After refactoring:iex(1)>Bird.plumage(%AfricanSwallow{number_of_coconuts:7})"tired"iex(2)>Bird.plumage(%NorwegianParrot{voltage:7000})"scorched"iex(3)>Bird.plumage(%EuropeanSwallow{})"average"

    After this refactoring, whenever we need to classify the plumage of a new bird type, we only need to create a module for that new type and implement theBird protocol in it.

    This example is based on an original code by Zack Kayser. Source:link

▲ back to Index


Functional Refactorings

Functional refactorings are those that use programming features characteristic of functional languages, such as pattern matching and higher-order functions. In this section, 32 different refactorings classified as functional are explained and exemplified:

Generalise a function definition

  • Category: Functional Refactorings.

  • Motivation: This refactoring helps to eliminate theDuplicated Code code smell. In any programming language, this code smell can make the codebase harder to maintain due to restrictions on code reuse. When different functions have equivalent expression structures, that structure should be generalized into a new function, which will later be called in the body of the duplicated functions, replacing their original codes. After that refactoring, the programmer only needs to worry about maintaining these expressions in one place (generic function). The support forhigher-order functions in functional programming languages enhances the potential for generalizing provided by this refactoring.

  • Examples: In Elixir, as well as in other functional languages such as Erlang and Haskell, functions are considered as first-class citizens. This means that functions can be assigned to variables, allowing the definition ofhigher-order functions. Higher-order functions are those that take one or more functions as arguments or return a function as a result. The following code illustrates this refactoring using ahigher-order function. Before the refactoring, we have two functions in theGen module. Thefoo/1 function takes a list as an argument and transforms it in two steps. First, it squares each of its elements and then multiplies each element by 3, returning a new list. Thebar/1 function operates similarly, receiving a list as an argument and also transforming it in two steps. First, it doubles the value of each element in the list and then returns a list containing only the elements divisible by 4. Although these two functions transform lists in different ways, they have duplicated structures.

    # Before refactoring:defmoduleGendodeffoo(list)dolist_comprehension=forx<-list,do:x*xlist_comprehension|>Enum.map(&(&1*3))enddefbar(list)dolist_comprehension=forx<-list,do:x+xlist_comprehension|>Enum.filter(&(rem(&1,4)==0))endend#...Use example...iex(1)>Gen.foo([1,2,3])[3,12,27]iex(2)>Gen.bar([2,3,4])[4,8]

    We want to generalize the functionsfoo/1 andbar/1. To do so, we must create a new functiongeneric/4 and replace the bodies offoo/1 andbar/1 with calls togeneric/4. Note thatgeneric/4 is ahigher-order function, since its last three arguments are functions that are called only within its body. Due to the use of higher-order functions in this refactoring, we were able to create a smaller and easier-to-maintain new function than would be if we did not use this functional programming feature.

    # After refactoring:defmoduleGendodefgeneric(list,generator_op,trans_op,trans_args)dolist_comprehension=forx<-list,do:generator_op.(x,x)list_comprehension|>trans_op.(trans_args)enddeffoo(list)do# Body replacedgeneric(list,&Kernel.*/2,&Enum.map/2,&(&1*3))enddefbar(list)do# Body replacedgeneric(list,&Kernel.+/2,&Enum.filter/2,&(rem(&1,4)==0))endend#...Use example...iex(1)>Gen.foo([1,2,3])[3,12,27]iex(2)>Gen.bar([2,3,4])[4,8]

    This refactoring preserved the behaviors offoo/1 andbar/1, without the need to modify their calls. In addition, we eliminated the duplicated code, allowing the developer to focus solely on maintaining the generic function in theGen module. Finally, if there is a need to create a new function for transforming lists in two steps, we can possibly reuse the code fromgeneric/4 without creating new duplications.

  • Side-conditions:

    • The name and arity of the higher-order function created in this refactoring (e.g.,generic/4) must not conflict with the name of any other function already defined or imported by the refactored module;

    • The functions that have equivalent expressions and are thus generalized by this refactoring must originally be defined in the same module;

    • The arity of the generic function created by this refactoring must be identical to the number of groups of generalizable code elements in the refactored equivalent expressions;

    • The equivalent expressions to be generalized are not patterns and do not invoke the function in which they are defined (i.e., recursive calls);

    • The extracted sequence of equivalent expressions to be generalized must not be part of amacro definition.

    These side conditions are based on definitions provided by members of the RefactorErl project, as described in the following link:[1]

▲ back to Index


Introduce pattern matching over a parameter

  • Category: Functional Refactorings.

  • Motivation: Some functions have different branches that depend on the values passed to one or more parameters at call time to define the flow of execution. When these functions use classic conditional structures to implement these branches (e.g.,if,unless,cond,case), they can get large bodies, becomingLong Functions, thus harming the maintainability of the code. This refactoring seeks to replace, when appropriate, these classic conditional structures that control the branches defined by parameter values, with pattern-matching features and multi-clause functions from functional languages such as Elixir, Erlang, and Haskell.

  • Examples: The following code presents thefibonacci/1 function. This recursive function has three different branches that are defined by the value of its single parameter, two for its base cases and one for its recursive case. The control flow for each of these branches is done by a classic conditional structure (case).

    # Before refactoring:deffibonacci(n)whenis_integer(n)docasendo0->01->1_->fibonacci(n-1)+fibonacci(n-2)endend#...Use examples...iex(1)>Foo.fibonacci(8)21

    We want to replace the classiccase conditional by using pattern matching on the function parameter. This will turn this function into a multi-clause function, assigning each branch to a clause. Note that in addition to reducing the size of the function body by distributing the branches across the clauses, it is not necessary to make any changes to thefibonacci/1 callers, since their behavior has been preserved.

    # After refactoring:deffibonacci(0),do:0deffibonacci(1),do:1deffibonacci(n)whenis_integer(n)dofibonacci(n-1)+fibonacci(n-2)end#...Use examples...iex(1)>Foo.fibonacci(8)21

    Important: Althoughfibonacci/1 is not aLong Functions and originally has simple expressions in each of its branches, it serves to illustrate the purpose of this refactoring. Try to imagine a scenario where a function has many different branches, each of which is made up of several lines of code. This would indeed be an ideal scenario to apply the proposed refactoring.

  • Side-conditions:

    • The function to be refactored must have parameters that receive values defining which internal branch will run. Thus, after the refactoring, we will have different clauses of the refactored function performing pattern matching on these parameters;

    • The number of clauses in the multi-clause function generated by this refactoring must be identical to the number of branches in the original function;

    • The expressions used in the pattern matches should be the same as those originally evaluated by the classical conditional structures.

▲ back to Index


Turning anonymous into local functions

  • Category: Functional Refactorings.

  • Motivation: In Elixir, as well as in other functional languages like Erlang and Haskell, functions are considered as first-class citizens, which means that they can be assigned to variables. This enables the creation of anonymous functions, also called lambda functions, that can be assigned to variables and called from them. Although anonymous functions are very useful in many situations, they have less potential for reuse than local functions and cannot, for example, be exported to other modules. When we encounter the same anonymous function being defined in different points of the codebase (Duplicated Code), these anonymous functions should be transformed into a local function, and the locations where the anonymous functions were originally implemented should be updated to use the new local function. In functional languages, this refactoring is also referred to as lambda lifting and is a specific instance ofExtract Function. With this refactoring, we can reduce occurrences of duplicated code, enhancing code reuse potential.

  • Examples: The following code illustrates this refactoring. Before the refactoring, we have two functions in theLambda module. Thefoo/1 function takes a list as an argument and doubles the value of each element, returning a new list. Thebar/1 function operates similarly, receiving a list as an argument and also doubles the value of each three elements, then returns a list. Note that both local functionsfoo/1 andbar/1 internally define the same anonymous functionfn x -> x * 2 end for doubling the desired list values.

    # Before refactoring:defmoduleLambdadodeffoo(list)doEnum.map(list,fnx->x*2end)enddefbar(list)doEnum.map_every(list,3,fnx->x*2end)endend#...Use examples...iex(1)>Lambda.foo([1,2,3])[2,4,6]iex(2)>Lambda.bar([3,4,5,6,7,8,9])[6,4,5,12,7,8,18]

    We want to avoid the duplicated implementation of anonymous functions. To achieve this, we will create a new local functiondouble/1, responsible for performing the same operation previously performed by the duplicated anonymous functions. In addition, the Elixir capture operator (&) will be used in the places offoo/1 andbar/1 which originally implement anonymous functions, to replace their use with the new local functiondouble/1.

    # After refactoring:defmoduleLambdadodefdouble(x)do#<- lambda lifted to a local function!x*2enddeffoo(list)doEnum.map(list,&double/1)enddefbar(list)doEnum.map_every(list,3,&double/1)endend#...Use examples...iex(1)>Lambda.foo([1,2,3])[2,4,6]iex(2)>Lambda.bar([3,4,5,6,7,8,9])[6,4,5,12,7,8,18]

    Note that although in this example the new local functiondouble/1, defined to replace the duplicated anonymous functions, was only used in theLambda module, nothing prevents it from being reused in other parts of the codebase, asdouble/1 can be imported by any other module.

  • Side-conditions:

    • The originally duplicated anonymous functions must have been defined within the same module (e.g.,Lambda).

    • The originally duplicated anonymous functions may have been defined using thefn andend syntax (e.g.,fn x -> x * 2 end), or the more compact equivalent syntax using the& operator (e.g.,&(&1 * 2)).

    • The name and arity of new local function created in this refactoring (e.g.,double/1) must not conflict with the name of any other function already defined or imported by the refactored module.

▲ back to Index


Merging multiple definitions

  • Category: Functional Refactorings.

  • Motivation: This refactoring is also an option for removingDuplicated Code and can optimize a codebase by sharing that code in a single location, avoiding multiple traversals over the same data structure. There are situations where a codebase may have distinct functions that are complementary. Because they are complementary, these functions may have identical code snippets. When identified, these functions should be merged into a new function that will simultaneously perform the processing done by the original functions separately. The new function created by this refactoring will always return a tuple, where each original return provided by the merged functions will be contained in different elements of the tuple returned by the new function. In functional languages, this refactoring is also referred to astupling.

  • Examples: The following code illustrates this refactoring. Before the refactoring, we have two functions in theMyList module. Thetake/2 function takes an integer valuen and a list as parameters, returning a new list composed of the firstn elements of the original list. Thedrop/2 function also takes an integer valuen and a list as parameters, but ignores the firstn elements of the original list, returning a new list composed of the remaining elements. Note that althoughtake/2 anddrop/2 return different values, they are complementary multi-clause functions and therefore have many nearly identical code snippets.

    # Before refactoring:defmoduleMyListdodeftake(0,_),do:[]deftake(_,[]),do:[]deftake(n,[h|t])whenn>0do[h|take(n-1,t)]enddeftake(_,_),do::error_take_negativedefdrop(0,list),do:listdefdrop(_,[]),do:[]defdrop(n,[h|t])whenn>0dodrop(n-1,t)enddefdrop(_,_),do::error_drop_negativeend#...Use examples...iex(1)>list=[1,2,3,4,5,6]iex(2)>MyList.take(2,list)[1,2]iex(3)>MyList.drop(2,list)[3,4,5,6]

    If we analyze the examples of using the code above, we can clearly see how these functions are complementary. Both received the same list as a parameter and by joining the lists returned by them, we will have the same elements of the original list, in other words, it is as if we had split the original list after the second element and ignored one of the two sub-lists in each of the functions.

    Thinking about this, we can merge these two complementary functions into a new function calledsplit_at/2. This function will remove duplicate expressions by introducing code sharing. In addition, it will return a tuple composed of two elements. The first element will contain the value that would originally be returned bytake/2 and the second element will contain the value that would be returned bydrop/2.

    # After refactoring:defmoduleMyListdodeftake(0,_),do:[]#<- can be deleted in the future!deftake(_,[]),do:[]deftake(n,[h|t])whenn>0do[h|take(n-1,t)]enddeftake(_,_),do::error_take_negativedefdrop(0,list),do:list#<- can be deleted in the future!defdrop(_,[]),do:[]defdrop(n,[h|t])whenn>0dodrop(n-1,t)enddefdrop(_,_),do::error_drop_negative# Merging take and drop!defsplit_at(0,list),do:{[],list}defsplit_at(_,[]),do:{[],[]}defsplit_at(n,[h|t])whenn>0do{ts,zs}=split_at(n-1,t){[h|ts],zs}enddefsplit_at(_,_),do:{:error_take_negative,:error_drop_negative}end#...Use examples...iex(1)>list=[1,2,3,4,5,6]iex(2)>MyList.split_at(2,list){[1,2],[3,4,5,6]}

    In the refactored code above, we kept the functionstake/2 anddrop/2 in theMyList module just so the reader could more easily compare how this merge allowed code sharing. They were not modified. From a practical point of view, the calls totake/2 anddrop/2 can be replaced by calls tosplit_at/2, with their respective returns being extracted via pattern matching, as in the example below:

    iex(1)>{take,drop}=MyList.split_at(2,[1,2,3,4,5,6])iex(2)>take[1,2]iex(3)>drop[3,4,5,6]

    The functionstake/2 anddrop/2 can be deleted fromMyList once all their calls have been replaced by calls tosplit_at/2.

    These examples are based on Haskell code written in two papers. Source:[1],[2].

  • Side-conditions:

    • The functions to be merged in this refactoring should preferably be originally defined in the same module. This way, in addition to ensuring that the transformed code maintains the same behavior, the cohesion of the codebase will also be preserved;

    • After this refactoring, if the calls to the original functions are replaced with a call to the newly created function, pattern matching is required to extract the desired values from the returnedtuple, ensuring that the original behavior of the code is preserved.

    These side conditions are based on definitions written by Tamás Kozsiket al. in this paper:[1]

▲ back to Index


Splitting a definition

  • Category: Functional Refactorings.

  • Motivation: This refactoring is the inverse ofMerging multiple definitions. While merge multiple definitions aims to group recursive functions into a single recursive function that returns a tuple, splitting a definition aims to separate a recursive function by creating new recursive functions, each of them responsible for individually generating a respective element originally contained in a tuple.

  • Examples: Take a look at the example inMerging multiple definitions in reverse order, that is,# After refactoring: -># Before refactoring:.

  • Side-conditions:

    • The function selected for refactoring must originally return atuple containing a number of elements identical to the number of new functions to be created by this refactoring;

    • The new recursive functions created by this refactoring must not conflict with functions of the same name and arity that are already defined or imported into the refactored module.

    These side conditions are based on definitions provided by members of the HaRe project (Haskell Refactorer tool), as described in the following link:[1]

▲ back to Index


Inline macro

  • Category: Functional Refactorings.

  • Note: Formerly known as "Inline macro substitution".

  • Motivation:Macros are powerful meta-programming mechanisms that can be used in Elixir, as well as other functional languages like Erlang and Clojure, to extend the language. However, when a macro is implemented to solve problems that could be solved by functions or other pre-existing language structures, the code becomes unnecessarily more complex and less readable. Therefore, when identifying unnecessary macros that have been implemented, we can replace all instances of these macros with the code defined in their bodies. Some code compensations will be necessary to ensure that they continue to perform properly after refactoring. This refactoring is a specialization of theInline function and can be used to remove the code smellUnnecessary Macros.

  • Examples: The following code illustrates this refactoring. Before the refactoring, we have a macrosum_macro/2 defined in theMyMacro module. This macro is used by thebar/2 function in theFoo module, unnecessarily complicating the code's readability.

    # Before refactoring:defmoduleMyMacrododefmacrosum_macro(v1,v2)doquotedounquote(v1)+unquote(v2)endendend
    # Before refactoring:defmoduleFoododefbar(v1,v2)dorequireMyMacroMyMacro.sum_macro(v1,v2)endend#...Use examples...iex(1)>Foo.bar(2,3)5

    To eliminate the unnecessary macroMyMacro.sum_macro/2, we will replace all its calls with its body, making some code adjustments. Then, we can deleteMyMacro.sum_macro/2 since it will no longer be used.

    # After refactoring:defmoduleFoododefbar(v1,v2)dov1+v2#<- inlined macro!endend#...Use examples...iex(1)>Foo.bar(2,3)5
  • Side-conditions:

    • The body of themacro, which is used to replace its calls, must not contain variables that conflict with the names of other variables already existing in the scope where themacro calls occurred;

    • The selectedmacro to have its calls replaced by its body must not contain anothermacro call in its definition;

    • During the replacement ofmacro calls with its body, we must clean up the code by removing constructs such asquote/2 andunquote/1, as they are unnecessary within the scope of a conventional function.

    These side conditions are based on definitions provided by members of the RefactorErl project, as described in the following link:[1]

▲ back to Index


Transforming list appends and subtracts

  • Category: Functional Refactorings.

  • Motivation: This is a refactoring that can make the code shorter and even more readable. Elixir'sEnum module provides native functions to append an element to the end of a list (concat/2) and to subtract elements (reject/2) from a list. Although these functions serve their purposes well, Elixir also has specific operators equivalent to these functions, allowing the code to be simplified. This refactoring aims to transform calls to theEnum.concat/2 andEnum.reject/2 functions into uses of theKernel.++/2 andKernel.--/2 operators, respectively.

  • Examples: The following code shows an example of this simple refactoring.Enum.concat/2 receives two lists as parameters and appends the elements of the second list to the end of the first. On the other hand, functionEnum.reject/2 receives a list and an anonymous function as parameters. This anonymous function is responsible for comparing each element of a second list with the elements of the first list, allowing the subtraction of elements present in both lists.

    # Before refactoring:iex(1)>Enum.concat([1,2,3,4],[5,6,7])[1,2,3,4,5,6,7]iex(2)>Enum.reject([1,2,3,4,5],&(Enum.member?([1,3],&1)))[2,4,5]

    We can replace the use of functions from theEnum module with their equivalent specific operators, greatly simplifying the code as shown below.

    # After refactoring:iex(1)>[1,2,3,4]++[5,6,7][1,2,3,4,5,6,7]iex(2)>[1,2,3,4,5]--[1,3][2,4,5]
  • Side-conditions:

    • To maintain the same behavior of the code, the list on the left side of theKernel.++/2 operator after the refactoring must be identical to the list that was originally passed as the first parameter of theEnum.concat/2 function. Additionally, the list on the right side ofKernel.++/2 must have originally been the second parameter provided in the call toEnum.concat/2;

    • When performing list subtraction, to maintain the same behavior, the list on the left side of theKernel.--/2 operator must have originally been the first parameter of theEnum.reject/2 call. On the other hand, the list on the right side ofKernel.--/2 must have originally been the one contained within the anonymous function passed as the second parameter of theEnum.reject/2 call.

▲ back to Index


From tuple to struct

  • Category: Functional Refactorings.

  • Motivation: In Elixir, as well as in other functional languages like Erlang and Haskell, tuples are one of the most commonly used data structures. They are typically used to group a small and fixed amount of values. Although they are very useful, using tuples can make code less readable, as some details of the data representation are exposed in the code due to the inability to name the elements that compose a tuple. This refactoring aims to transform tuples into structs, which are data structures that allow naming their fields, thus providing a more abstract interface for the data and improving the code readability.

  • Examples: The following code shows an example of this refactoring. Before the refactoring, thediscount/2 function of theOrder module receives a tuple composed of order data and a discount percentage to be applied to the total value of the order. This function applies the discount to the total value of the order and returns a new tuple with the updated value.

    # Before refactoring:defmoduleOrderdodefdiscount(tuple,value)doput_elem(tuple,2,elem(tuple,2)*value)endend#...Use examples...iex(1)>Order.discount({:s1,"Jose",150.0},0.5){:s1,"Jose",75.0}

    We can replace the use of this tuple by creating a struct%Order{} that contains the named data of an order, abstracting the interface for accessing this data and improving the readability of the code.

    # After refactoring:defmoduleOrderdodefstruct[id:nil,customer:nil,date:nil,total:nil]defdiscount(order,value)do%Order{order|total:order.total*value}endend#...Use examples...iex(1)>Order.discount(%Order{id::s1,customer:"Jose",total:150.0},0.5)%Order{id::s1,customer:"Jose",total:75.0}
  • Side-conditions:

    • The module containing thetuple chosen for refactoring must not define astruct prior to the refactoring, to avoid conflicts;

    • The names of thestruct fields created during the refactoring must beatoms, and therefore must all be unique.

    These side conditions are based on definitions written by Huiqing Liet al. in this paper:[1]

▲ back to Index


Struct guard to matching

  • Category: Functional Refactorings.

  • Motivation: In Elixir, as well as in other functional languages like Erlang and Haskell, guards are mechanisms used to perform more complex checks that would not be possible to do using just pattern matching. Although very useful, when guards are used unnecessarily to perform checks that could be done with just pattern matching, the code can become verbose and less readable. This refactoring focuses on transforming the use ofis_struct/1 oris_struct/2 function calls into guards, for explicit pattern matching usage.

  • Examples: The following code shows an example of this refactoring. Before the refactoring, thediscount/2 function of theOrder module use theis_struct/2 function to check if their first parameter is a struct of typeOrder.

    # Before refactoring:defmoduleOrderdodefdiscount(struct,value)whenis_struct(struct,Order)do%Order{struct|total:struct.total*value}endend#...Use examples...iex(1)>Order.discount(%Order{id::s1,customer:"Jose",total:150.0},0.5)%Order{id::s1,customer:"Jose",total:75.0}iex(2)>Order.discount(%{},0.5)#<= Used Map instead Struct!**(FunctionClauseError) no function clause matchinginOrder.discount/2

    Since the check performed by theis_struct/2 guard is simple, it can be replaced by directly using pattern matching on the first parameter of thediscount/2.

    # After refactoring:defmoduleOrderdodefdiscount(%Order{}=struct,value)do%Order{struct|total:struct.total*value}endend#...Use examples...iex(1)>Order.discount(%Order{id::s1,customer:"Jose",total:150.0},0.5)%Order{id::s1,customer:"Jose",total:75.0}iex(2)>Order.discount(%{},0.5)#<= Used Map instead Struct!**(FunctionClauseError) no function clause matchinginOrder.discount/2
  • Side-conditions:

    • To maintain the same behavior of the code, in the refactored version, an instance of the struct that was originally checked by theis_struct function (e.g.,%Order{}) must be placed on the left side of the match operator in the pattern matching performed on the parameter that originally only received a value validated by the guard (i.e.,struct).

▲ back to Index


Struct field access elimination

  • Category: Functional Refactorings.

  • Motivation: In Elixir, as well as in other functional languages like Erlang and Haskell, it's possible for a function to receivestructs, or equivalent data types, as parameters and then access fields of these structs within its body or even in its signature to perform checks in guards. This refactoring focuses on replacing direct access to fields of a struct with the use of temporary variables that hold values extracted from these fields, which can then reduce the size of the code.

  • Examples: The following code shows an example of this refactoring. Before the refactoring, thediscount/2 function of theOrder module accesses thetotal field of thestruct received as a parameter in two places, first to perform a guard check in its signature and then within its body to calculate the new value.

    # Before refactoring:defmoduleOrderdodefdiscount(%Order{}=struct,value)whenstruct.total>=100.0do%Order{struct|total:struct.total*value}endend#...Use examples...iex(1)>Order.discount(%Order{id::s1,customer:"Jose",total:150.0},0.5)%Order{id::s1,customer:"Jose",total:75.0}

    To reduce the size of this code, we can use pattern matching to extract the value of thetotal field to a temporary variablet and replace all direct accesses to thetotal field with the use of the variablet.

    # After refactoring:defmoduleOrderdodefdiscount(%Order{total:t}=struct,value)whent>=100.0do%Order{struct|total:t*value}endend#...Use examples...iex(1)>Order.discount(%Order{id::s1,customer:"Jose",total:150.0},0.5)%Order{id::s1,customer:"Jose",total:75.0}

    Note that the more direct accesses to a field of a struct exist before refactoring, the more benefits this refactoring will bring in reducing the size of the code.

    When struct fields are accessed exclusively in the function signature or its body, we must be careful not to introduce the code smellComplex extractions in clauses with this refactoring.

  • Side-conditions:

    • The variable created to replace the direct access to struct fields must have a name that does not conflict with the names of parameters or local variables defined in the function before the refactoring.

▲ back to Index


Equality guard to pattern matching

  • Category: Functional Refactorings.

  • Motivation: This refactoring can further reduce Elixir code generated by theStruct field access elimination refactoring. When a temporary variable extracted from a struct field is only used in an equality comparison in a guard, extracting and using that variable is unnecessary, as we can perform that equality comparison directly with pattern matching.

  • Examples: The following code shows an example of this refactoring. Before the refactoring, we have a functiondiscount/2 that allows reducing the cost of purchases that cost at least100.0 and are made by customers named"Jose".

    # Before refactoring:defmoduleOrderdodefdiscount(%Order{total:t,customer:c}=s,value)whent>=100.0andc=="Jose"do%Order{s|total:t*value}endend#...Use examples...iex(1)>Order.discount(%Order{id::s1,customer:"Jose",total:150.0},0.5)%Order{id::s1,customer:"Jose",total:75.0}iex(2)>Order.discount(%Order{id::s1,customer:"Maria",total:150.0},0.5)**(FunctionClauseError) no function clause matchinginOrder.discount/2

    Note that the variablec was extracted from thecustomer field of theOrder struct. Additionally, this variablec is only used in an equality comparison in the guard. Therefore, we can eliminate the variablec and replace the equality comparison with a pattern matching on thecustomer field of the struct received in the first parameter of thediscount/2 function.

    # After refactoring:defmoduleOrderdodefdiscount(%Order{total:t,customer:"Jose"}=s,value)whent>=100.0do%Order{s|total:t*value}endend#...Use examples...iex(1)>Order.discount(%Order{id::s1,customer:"Jose",total:150.0},0.5)%Order{id::s1,customer:"Jose",total:75.0}iex(2)>Order.discount(%Order{id::s1,customer:"Maria",total:150.0},0.5)**(FunctionClauseError) no function clause matchinginOrder.discount/2
  • Side-conditions:

    • The temporary variable replaced by a pattern matching occurrence must have been originally created via extraction from a composite data type (e.g.,list,tuple,Map, orStruct) performed in a function clause;

    • Before the refactoring, this temporary variable must be exclusively used in an equality comparison performed in a guard clause;

    • After the refactoring, the location where the temporary variable was originally extracted must have its creation replaced by the value that was originally compared to it in the guard clause before the refactoring.

▲ back to Index


Static structure reuse

  • Category: Functional Refactorings.

  • Motivation: When identical tuples or lists are used at different points within a function, they are unnecessarily recreated by Elixir. This not only makes the code more verbose but also takes up more memory space and can lead to less efficient runtime. This refactoring aims to eliminate these unnecessary recreations of identical static structures by assigning them to variables that allow these structures to be shared throughout the code.

  • Examples: The following code shows an example of this refactoring. Before the refactoring, we have a functioncheck/1. This function receives a two-elementtuple as a parameter, where the first element is a boolean value indicating whether payment for an order has been confirmed, and the second element is alist of items that make up the order.

    # Before refactoring:defmoduleOrderdodefcheck({paid,[:car,:house,i]})docasepaiddotrue->[:car,:house,i]false->{paid,[:car,:house,i]}endendend#...Use examples...iex(1)>Order.check({true,[:car,:house,:boat]})[:car,:house,:boat]iex(2)>Order.check({false,[:car,:house,:boat]}){false,[:car,:house,:boat]}

    Note that there is atuple and alist being recreated in this function. When the payment is confirmed,check/1 recreates and returns thelist of items in the order (the second element of thetuple). On the other hand, when the payment has not yet been made,check/1 recreates the entiretuple received as a parameter and returns it.

    As shown in the following code, we can use pattern matching to create the variableslist andtuple in thecheck/1 clause, assigning these variables to the respective structures that were previously being unnecessarily recreated in the function body.

    # After refactoring:defmoduleOrderdodefcheck({paid,[:car,:house,_i]=list}=tuple)docasepaiddotrue->list# <= reuse!false->tuple# <= reuse!endendend#...Use examples...iex(1)>Order.check({true,[:car,:house,:boat]})[:car,:house,:boat]iex(2)>Order.check({false,[:car,:house,:boat]}){false,[:car,:house,:boat]}
  • Side-conditions:

    • The variables created to promote the reuse of static structures must have names that do not conflict with the names of pre-existing variables;

    • The static structures that are replaced by variables must be originally lexically identical.

▲ back to Index


Simplifying guard sequences

  • Category: Functional Refactorings.

  • Motivation: The guard clauses in Elixir may contain redundant logical propositions. Although this does not cause behavioral problems for the code, it can make it more verbose and inefficient. This refactoring aims to simplify the guard clauses by eliminating redundancies whenever possible.

  • Examples: The following code shows an example of this refactoring. Before the refactoring, the multi-clause functionbar/1 has redundant guards in both clauses. The first clause checks if a parameter is of thefloat type and also equals a constant of that type. The second clause checks if a parameter is of thelist type and if thislist has more than two values.

    # Before refactoring:defmoduleFoododefbar(f)whenis_float(f)andf==81.0do{:float,f}enddefbar(l)whenis_list(l)andlength(l)>2do{:list,l}endend#...Use examples...iex(1)>Foo.bar(81.0){:float,81.0}iex(2)>Foo.bar(81)#<= integer!**(FunctionClauseError) no function clause matchinginFoo.bar/1iex(3)>Foo.bar([1,2,3,4]){:list,[1,2,3,4]}iex(4)>Foo.bar({1,2,3,4})#<= tuple!**(FunctionClauseError) no function clause matchinginFoo.bar/1

    As shown in the following code, we can simplify the first clause by using the strict equality comparison operator. The second clause can be simplified by using only theKernel.length/1 function, since it expects a parameter of thelist type. If a parameter of a different type is passed toKernel.length/1, the pattern matching for that clause will not be performed.

    # After refactoring:defmoduleFoododefbar(f)whenf===81.0do{:float,f}enddefbar(l)whenlength(l)>2do{:list,l}endend#...Use examples...iex(1)>Foo.bar(81.0){:float,81.0}iex(2)>Foo.bar(81)#<= integer!**(FunctionClauseError) no function clause matchinginFoo.bar/1iex(3)>Foo.bar([1,2,3,4]){:list,[1,2,3,4]}iex(4)>Foo.bar({1,2,3,4})#<= tuple!**(FunctionClauseError) no function clause matchinginFoo.bar/1
  • Side-conditions:

    • The expressions to be simplified by this refactoring must originally be defined in guards, whether they are for checks incase statements or in function clauses;

    • The expressions to be refactored, in order to be considered redundant, must alternatively:

      • check the type of a simple value (e.g.,integer,float, oratom) and additionally perform anequality comparison of this value with a constant;

      • check the type of a composite value (e.g.,list,tuple,bitstring, orMap) and also perform a comparison of this value with a constant in such a way that the type is inherently verified as well. For example, the functionsbyte_size/1,tuple_size/1, andmap_size/1, in addition to returning the sizes of composite values, also intrinsically verify if they are of the typesbitstring,tuple, orMap, respectively.

▲ back to Index


Converts guards to conditionals

  • Category: Functional Refactorings.

  • Motivation: In Elixir, we can differentiate each clause of a function by using guards. The goal of this refactoring is to replace all guards in a function with traditional conditionals, creating only one clause for the function.

  • Examples: The following code shows an example of this refactoring. Before the refactoring, we have a multi-clause functionbar/1. The first clause checks if a parameter is of thefloat type and also equals a constant of that type. The second clause checks if a parameter is of thelist type and if thislist has more than two values.

    # Before refactoring:defmoduleFoododefbar(f)whenf===81.0do{:float,f}enddefbar(l)whenlength(l)>2do{:list,l}endend#...Use examples...iex(1)>Foo.bar(81.0){:float,81.0}iex(2)>Foo.bar(81)#<= integer!**(FunctionClauseError) no function clause matchinginFoo.bar/1iex(3)>Foo.bar([1,2,3,4]){:list,[1,2,3,4]}iex(4)>Foo.bar({1,2,3,4})#<= tuple!**(FunctionClauseError) no function clause matchinginFoo.bar/1

    As shown in the following code, we can replace the two guards with acond conditional, creating only one clause for thebar/1 function.

    # After refactoring:defmoduleFoododefbar(v)dotrydoconddov===81.0->{:float,v}length(v)>2->{:list,v}true->raiseFunctionClauseErrorendrescue_einArgumentError->raiseFunctionClauseErrorendendend#...Use examples...iex(1)>Foo.bar(81.0){:float,81.0}iex(2)>Foo.bar(81)#<= integer!**(FunctionClauseError) no function clause matchesinFoo.bar/1iex(3)>Foo.bar([1,2,3,4]){:list,[1,2,3,4]}iex(4)>Foo.bar({1,2,3,4})#<= tuple!**(FunctionClauseError) no function clause matchesinFoo.bar/1

    In Elixir, when an error is raised from inside the guard, it won’t be propagated, and the guard expression will just return false. An example of this occurs when a call toKernel.length/1 in a guard receives a parameter that is not alist. Instead of propagating anArgumentError, the corresponding clause just won’t match. However, when the same proposition is used outside of a guard (in a conditional), anArgumentError will be propagated.

    To keep the refactored code with the same behavior as the original, raising only aFunctionClauseError when the conditional has no branch equivalent to the desired clause, it was necessary to use the error-handling mechanism of Elixir. Note that the use of this error-handling mechanism, combined with the fact of merging multiple clauses into one, may turn this refactored code into aLong Function.

  • Side-conditions:

    • The guards to be transformed into clauses of acond statement should originally be used to differentiate clauses of a multi-clause function;

    • After the refactoring, it may be necessary to use error-handling mechanisms to maintain the same original behavior of the code. As explained earlier, errors that were not propagated in the guards may now be propagated outside of them.

▲ back to Index


Widen or narrow definition scope

  • Category: Functional Refactorings.

  • Motivation: In Elixir, it is not possible to define nested named functions, however, it is possible to define a nested anonymous function (inside) of a named function. In this case, the anonymous function's scope is narrowed to the body of the named function where it was defined. This refactoring aims to widen or narrow a function's usage scope.

  • Examples: The following code examples illustrate the widening of a function's scope. Prior to refactoring, the moduleFoo has the definition of the named functionbar/3. Within this named function, we have the definition of the nested anonymous functionmy_div/2. Note that the scope of themy_div/2 function is narrowed to the body of thebar/3 function.

    # Before refactoring:defmoduleFoododefbar(v1,v2,v3)domy_div=fn(_,0)->{:error,"invalid!"}(x,y)->{:ok,x/y}endcasemy_div.(v1,v2)do{:error,msg}->msg{:ok,value}->value*v3endendend#...Use examples...iex(1)>Foo.bar(10,0,5)"invalid!"iex(2)>Foo.bar(10,2,5)25.0iex(3)>my_div.(10,2)warning:variable"my_div"doesnotexist...**(CompileError) undefined function my_div/0...

    To widen the scope of the anonymous functionmy_div/2, we can transform it into a named function defined outside ofbar/3. In addition, we must replace all calls to the anonymous functionmy_div/2 with calls to the newly named functionmy_div/2, as shown below.

    # After refactoring:defmoduleFoododefbar(v1,v2,v3)docasemy_div(v1,v2)do{:error,msg}->msg{:ok,value}->value*v3endend# new multi-clause named function with widened scope!defmy_div(_,0),do:{:error,"invalid!"}defmy_div(x,y),do:{:ok,x/y}end#...Use examples...iex(1)>Foo.bar(10,0,5)"invalid!"iex(2)>Foo.bar(10,2,5)25.0iex(3)>Foo.my_div(10,2){:ok,5.0}iex(4)>Foo.my_div(10,0){:error,"invalid!"}

    Considering this example, to narrow the scope ofmy_div/2, we can perform the reverse refactoring, that is,# After refactoring: -># Before refactoring:.

  • Side-conditions:

    • When widening the scope of an anonymous function, the name and arity of the newly named function created in this refactoring (e.g.,my_div/2) must not conflict with the name of any other function already defined or imported by the refactored module.

    • When widening the scope of an anonymous function, the anonymous function must not be a closure (i.e., it must not access variables outside its scope).

▲ back to Index


Introduce Enum.map/2

  • Category: Functional Refactorings.

  • Motivation: The divide-and-conquer pattern refers to a computation in which a problem is recursively divided into independent subproblems, and then the subproblems' solutions are combined to obtain the solution of the original problem. Such a computation pattern can be easily parallelized because we can work on the subproblems independently and in parallel. This refactoring aims to restructure functions that utilize the divide-and-conquer pattern, making parallelization easier. Specifically, this refactoring allows us to replace a list expression in which each element is generated by calling the same function with a call to the higher-order functionEnum.map/2.

  • Examples: The following code examples demonstrate this refactoring. Before the refactoring, thebar/2 function generates a list composed of two lists sorted by themerge_sort/1 function.

    # Before refactoring:defmoduleFoododefbar(list,list_2)do[merge_sort(list),merge_sort(list_2)]endend#...Use examples...iex(1)>Foo.bar([1,3,9,0,2],[90,-5,0,10,8])[[0,1,2,3,9],[-5,0,8,10,90]]

    After refactoring,bar/2 retains the same behavior, but now uses theEnum.map/2 function to generate the elements of the returned list.

    # After refactoring:defmoduleFoododefbar(list,list_2)doEnum.map([list,list_2],&merge_sort/1)endend#...Use examples...iex(1)>Foo.bar([1,3,9,0,2],[90,-5,0,10,8])[[0,1,2,3,9],[-5,0,8,10,90]]

    Note that this refactoring produces code that enables the application ofTransform to list comprehension.

  • Side-conditions:

    • For this refactoring to be performed without causing changes in the code's behavior, the function that originally generates each element of the list expression (e.g.,merge_sort/1) must be pure, meaning it should not produce side effects.

    This side condition is based on definitions written by Tamás Kozsiket al. in this paper:[1]

▲ back to Index


Merging match expressions into a list pattern

  • Category: Functional Refactorings.

  • Note: Formerly known as "Bindings to List".

  • Motivation: The divide-and-conquer pattern refers to a computation in which a problem is recursively divided into independent subproblems, and then the subproblems' solutions are combined to obtain the solution of the original problem. Such a computation pattern can be easily parallelized because we can work on the subproblems independently and in parallel. This refactoring aims to restructure functions that utilize the divide-and-conquer pattern, making parallelization easier. More precisely, this refactoring merges a series of match expressions into a single match expression that employs a list pattern.

  • Examples: The following code examples demonstrate this refactoring. Before the refactoring, thebar/2 function has a sequence of two match expressions that use themerge_sort/1 function.

    # Before refactoring:defmoduleFoododefbar(list,list_2)doe_1=merge_sort(list)e_2=merge_sort(list_2)# do something with e_1 and e_2 ...endend

    After refactoring,bar/2 retains the same behavior, but now uses a single match expression with a list pattern.

    # After refactoring:defmoduleFoododefbar(list,list_2)do[e_1,e_2]=[merge_sort(list),merge_sort(list_2)]# do something with e_1 and e_2 ...endend

    Note that this refactoring produces code that enables the application ofIntroduce Enum.map/2.

  • Side-conditions:

    • For this refactoring to be performed without causing changes in the code's behavior, the functions that originally compose the series of match expressions must be pure, meaning they should not produce side effects. In the example above, we have onlymerge_sort/1, but this sequence of match expressions could also involve multiple different functions.

    This side condition is based on definitions written by Tamás Kozsiket al. in this paper:[1]

▲ back to Index


Function clauses to/from case clauses

  • Category: Functional Refactorings.

  • Motivation: The divide-and-conquer pattern refers to a computation in which a problem is recursively divided into independent subproblems, and then the subproblems' solutions are combined to obtain the solution of the original problem. Such a computation pattern can be easily parallelized because we can work on the subproblems independently and in parallel. This refactoring aims to restructure functions that utilize the divide-and-conquer pattern, making parallelization easier. More precisely, this refactoring allows transforming a multi-clause function into a single-clause function, mapping function clauses into clauses of acase statement. The reverse can also occur, i.e., mapping acase statement clause into function clauses, thus transforming a single-clause function into a multi-clause function (seeIntroduce pattern matching over a parameter).

  • Examples: The following code examples demonstrate this refactoring. Before the refactoring, themerge_sort/1 function has three clauses, with two for its base cases and one for its recursive case.

    # Before refactoring:defmoduleFoododefmerge_sort([]),do:[]#<= base casedefmerge_sort([h]),do:[h]#<= base casedefmerge_sort(list)do#<= recursive casehalf=length(list)|>div(2)right=merge_sort(Enum.take(list,half))left=merge_sort(Enum.drop(list,half))merge(right,left)endend#...Use examples...iex(1)>Foo.merge_sort([3,20,9,2,7,99,80,30])[2,3,7,9,20,30,80,99]

    After the refactoring,merge_sort/1 retains the same behavior, but now having only one clause, since both its base cases and recursive case were mapped into clauses of acase statement.

    # After refactoring:defmoduleFoododefmerge_sort(list)docaselistdo[]->[][h]->[h]_->half=length(list)|>div(2)right=merge_sort(Enum.take(list,half))left=merge_sort(Enum.drop(list,half))merge(right,left)endendend#...Use examples...iex(1)>Foo.merge_sort([3,20,9,2,7,99,80,30])[2,3,7,9,20,30,80,99]

    Note that this refactoring example could also be done in reverse order, that is,# After refactoring: -># Before refactoring:.

  • Side-conditions:

    • Once all clauses of a multi-clause function are unified into a single function that uses acase statement, for this refactoring to be performed properly, it is ideal that there are no identically named variables originally defined in the multiple clauses of the function. This prevents conflicts after unification.

    This side condition is based on definitions written by Tamás Kozsiket al. in this paper:[1]

▲ back to Index


Transform a body-recursive function to a tail-recursive

  • Category: Functional Refactorings.

  • Motivation: In Erlang and Elixir, there are two common styles for writing recursive functions: body-recursion and tail-recursion. Body-recursion allows for the recursive call to occur anywhere within the function body, while tail-recursion specifies that the recursive call must be the last operation performed before returning. To implement a tail-recursive function, an accumulating parameter is often used to store the intermediate results of the computation. When a tail-recursive function calls itself, the Erlang VM can perform a clever optimization technique known as tail-call optimization. This means that the function can continue without waiting for its recursive call to return. This optimization can enhance code parallelization and lead to more efficient code. To take advantage of the tail-call optimization provided by the Erlang VM, this refactoring aims to convert a body-recursive function into a tail-recursive one.

  • Examples: The code examples below illustrate this refactoring. Prior to the refactoring, thesum_list_elements/1 function uses body-recursion to sum all elements in a list.

    # Before refactoring:defmoduleFoododefsum_list_elements([]),do:0defsum_list_elements([head|tail])dosum_list_elements(tail)+headendend#...Use examples...iex(1)>Foo.sum_list_elements([1,2,3,4,5,6])21

    Following the refactoring,sum_list_elements/1 retains the same behavior but now uses tail-recursion to sum all elements in a list. Note that a private recursive functiondo_sum_list_elements/2 was created to support this refactoring.

    # After refactoring:defmoduleFoododefsum_list_elements(list)dodo_sum_list_elements(list,0)enddefpdo_sum_list_elements([],sum),do:sumdefpdo_sum_list_elements([head|tail],sum)dodo_sum_list_elements(tail,sum+head)endend#...Use examples...iex(1)>Foo.sum_list_elements([1,2,3,4,5,6])21

    By using theBenchee library for conducting micro benchmarking in Elixir, we can highlight the performance improvement potential of this refactoring. In the following code, thesum_list_elements/1 function is given illustrative names,before_ref/1 andafter_ref/1, to represent their respective body-recursive and tail-recursive versions.

    list=Enum.to_list(1..1_000_000)Benchee.run(%{"body_recursive"=>fn->Foo.before_ref(list)end,"tail_recursive"=>fn->Foo.after_ref(list)end},parallel:4)

    Note that for a list with one million elements, the tail-recursive version can be about three times faster than the body-recursive version.

    Operating System: macOSCPU Information: Intel(R) Core(TM) i7-4578U CPU @ 3.00GHzNumber of Available Cores: 4Available memory: 16 GBElixir 1.14.3Erlang 25.2Benchmark suite executing with the following configuration:warmup: 2 stime: 5 smemory time: 0 nsreduction time: 0 nsparallel: 4inputs: none specifiedEstimated total run time: 14 sBenchmarking body_recursive ...Benchmarking tail_recursive ...Name                     ips        average  deviation         median         99th %tail_recursive        180.22        5.55 ms    ±32.49%        5.42 ms       15.36 msbody_recursive         46.39       21.56 ms    ±50.78%       20.13 ms       52.23 msComparison: tail_recursive        180.22body_recursive         46.39 - 3.89x slower +16.01 ms
  • Side-conditions:

    • The new function created to support the transformation of the body-recursive function into a tail-recursive one (e.g.,do_sum_list_elements/2) must have a name that is different from all other functions already pre-existing or imported by the module to be refactored;

    • To be eligible for refactoring, a body-recursive function must not rely on post-recursive processing. In other words, it should be possible to transform this function solely with logical restructuring, without requiring design changes (e.g., types or the number of parameters).

▲ back to Index


Eliminate single branch

  • Category: Functional Refactoring.

  • Motivation: This refactoring aims to simplify the code by eliminating control statements that have only one possible flow.

  • Examples: The following code shows an example of this refactoring. Before the refactoring, we have a functionqux/1 with acase statement that has only one branch. When the pattern matching of this single branch does not occur, this function raises aCaseClauseError.

    # Before refactoring:defmoduleFoododefqux(value)docasevaluedo{:ok,v1,v2}->(v1+v1)*v2endendend#...Use examples...iex(1)>Foo.qux({:ok,2,4})16iex(2)>Foo.qux({:error,2,4})**(CaseClauseError)nocaseclausematching:{:error,2,4}

    As shown in the following code, we can simplify this code by replacing thecase statement with the code that would be executed by their single branch.

    # After refactoring:defmoduleFoododefqux(value)do{:ok,v1,v2}=value(v1+v1)*v2endend#...Use examples...iex(1)>Foo.qux({:ok,2,4})16iex(2)>Foo.qux({:error,2,4})**(MatchError)nomatchofrighthandsidevalue:{:error,2,4}

    Note that the only behavioral difference between the original and refactored code is that a different error is raised when the pattern matching does not occur (i.e.,MatchError). This could be compensated for by using the error-handling mechanism of Elixir, as shown inConverts guards to conditionals.

  • Side-conditions:

    • There are no preconditions for this refactoring;

    • Since different types of errors are raised between the two versions of the code (i.e.,CaseClauseError andMatchError), fully preserving the original behavior—including this aspect—may require compensation through error handlers (e.g.,try..rescue).

    These side conditions are based on definitions written by Tamás Kozsiket al. in this paper:[1]

▲ back to Index


Transform to list comprehension

  • Category: Functional Refactorings.

  • Motivation: Elixir, like Erlang, provides several built-inhigher-order functions capable of taking lists as parameters and returning new lists modified from the original. In Elixir,Enum.map/2 takes a list and an anonymous function as parameters, creating a new list composed of each element of the original list with values altered by applying the anonymous function. On the other hand, the functionEnum.filter/2 also takes a list and an anonymous function as parameters but creates a new list composed of elements from the original list that pass the filter established by the anonymous function. A list comprehension is another syntactic construction capable to create a list based on existing ones. This feature is based on the mathematical notation for defining sets and is very common in functional languages such as Haskell, Erlang, Clojure, and Elixir. This refactoring aims to transform calls toEnum.map/2 andEnum.filter/2 into list comprehensions, creating a semantically equivalent code that can be more declarative and easy to read.

  • Examples: The following code shows an example of this refactoring. Before the refactoring, we are usingEnum.map/2 to create a new list containing the elements of the original list squared. Furthermore, we are usingEnum.filter/2 to create a new list containing only the even numbers present in the original list.

    # Before refactoring:iex(1)>Enum.map([2,3,4],&(&1*&1))[4,9,16]iex(2)>Enum.filter([1,2,3,4,5],&(rem(&1,2)==0))[2,4]

    We can replace the use ofEnum.map/2 andEnum.filter/2 with the creation of semantically equivalent list comprehensions in Elixir, making the code more declarative as shown below.

    # After refactoring:iex(1)>forx<-[2,3,4],do:x*x[4,9,16]iex(2)>forx<-[1,2,3,4,5],rem(x,2)==0,do:x[2,4]
  • Side-conditions:

    • The only precondition for performing this refactoring is that calls to the functionsEnum.map/2 orEnum.filter/2 are selected to be converted into equivalent list comprehensions.

    This side condition is based on definitions provided by members of the RefactorErl project, as described in the following link:[1]

▲ back to Index


Nested list functions to comprehension

  • Category: Functional Refactorings.

  • Motivation: This refactoring is a specific instance ofTransform to list comprehension. WhenEnum.map/2 andEnum.filter/2 are used in a nested way to generate a new list, the code readability is compromised, and we also have an inefficient code, since the original list can be visited more than once and an intermediate list needs to be built. This refactoring, also referred to asdeforestation, aims to transform nested calls toEnum.map/2 andEnum.filter/2 into a list comprehension, creating a semantically equivalent code that can be more readable and more efficient.

  • Examples: The following code shows an example of this refactoring. Before the refactoring, we are usingEnum.map/2 andEnum.filter/2 in a nested way to create a new list containing only the even elements of the original list squared

    # Before refactoring:iex(1)>Enum.filter([1,2,3,4,5],&(rem(&1,2)==0))|>Enum.map(&(&1*&1))[4,16]

    We can replace these nested calls with the creation of semantically equivalent list comprehension in Elixir, making the code more declarative and efficient as shown below.

    # After refactoring:iex(1)>forx<-[1,2,3,4,5],rem(x,2)==0,do:x*x[4,16]
  • Side-conditions:

    • The only precondition for performing this refactoring is that nested calls to the functionsEnum.map/2 orEnum.filter/2 are selected to be converted into equivalent list comprehensions. However, it is not recommended to select too many levels of nesting for the conversion (e.g., more than three nested calls), as the list comprehension version may become even less readable than the original code.

▲ back to Index


List comprehension simplifications

  • Category: Functional Refactorings.

  • Motivation: This refactoring is the inverse ofTransform to list comprehension andNested list functions to comprehension. We can apply this refactoring to existing list comprehensions in the Elixir codebase, transforming them into semantically equivalent calls to the functionsEnum.map/2 orEnum.filter/2.

  • Examples: Take a look at the examples inTransform to list comprehension andNested list functions to comprehension in reverse order, that is,# After refactoring: -># Before refactoring:.

  • Side-conditions:

    • The list comprehension to be transformed can have only one generator. This precondition exists because transforming more complex list comprehensions,i.e., with two or more generators, although possible, would likely complicate the understanding of the code, making it not worth doing.

    This side condition is based on definitions provided by members of the RefactorErl project, as described in the following link:[1]

▲ back to Index


Closure conversion

  • Category: Functional Refactorings.

  • Source: This refactoring emerged from an extended Systematic Literature Review (SLR).

  • Motivation: This refactoring involves transformingclosures (i.e., anonymous functions that access variables outside their scope) into functions that receive the referenced variables as parameters. This transformation is beneficial for code optimization, enhancing memory management, simplifying the code's logical understanding, and improving its readability.

  • Examples: In this example,generate_sum/1 is ahigher-order function because it returns an anonymous function. The returned anonymous function is aclosure since it uses a variable that was defined outside its scope (i.e., variablex).

    # Before refactoring:defmoduleFoododefgenerate_sum(x)dofny->x+yend# closure example!endend#...Use example...iex(1)>add_8=Foo.generate_sum(8)#Function<0.104673823/1 in Foo.generate_sum/1>iex(2)>result=add_8.(2)10iex(3)>result=add_8.(5)13

    After the call toFoo.generate_sum(8), the variablex will always have the value8 in the anonymous function assigned toadd_8. This can be observed when this anonymous function is called with different values for its parametery (e.g.,2 and5). To optimize and improve code readability, we can perform aclosure conversion, makingx a parameter of the anonymous function returned bygenerate_sum/1, thus defining it within its scope. This refactoring, in this context, acts as a specific type ofAdd or remove a parameter applied to an anonymous function. Therefore, since the arity of the anonymous function has been modified, calls to this anonymous function also need to be updated, as shown below.

    # After refactoring:defmoduleFoododefgenerate_sum(_x)dofnx,y->x+yend# closure conversion!endend#...Use example...iex(1)>add_8=Foo.generate_sum(8)# unnecessary parameter in generate_sum/1#Function<0.7062781/2 in Foo.generate_sum/1>iex(2)>result=add_8.(8,2)10iex(3)>result=add_8.(2,2)4

    Note that this refactored code still presents opportunities to apply of other refactoring strategies. Since the parameter ofgenerate_sum/1 is no longer needed as it is always ignored within the function, we can applyAdd or remove a parameter togenerate_sum/1, transforming it intogenerate_sum/0. Additionally, we can useRename an identifier to update the name of the variableadd_8, responsible for binding the anonymous function returned by the higher-order function. As both values summed by the anonymous function are now defined at the time of its call, the nameadd_8 no longer makes sense.

  • Side-conditions:

    • The new parameter added to the anonymous function must have the same name as the variable that was previously coming from outside the scope of the anonymous function (e.g.,x). This will ensure that, in addition to the anonymous function no longer being a closure, the added parameter will not conflict with any other pre-existing parameter or local variable within the anonymous function.

▲ back to Index


Replace pipeline with a function

  • Category: Functional Refactorings.

  • Source: This refactoring emerged from a Grey Literature Review (GLR).

  • Motivation: When utilizing a pipeline composed of built-in higher-order functions to transform data, we may unnecessarily generate large and inefficient code. This refactoring aims to replace this kind of pipeline with a function call that composes it or by invoking another built-in function with equivalent behavior. In both cases, this refactoring will reduce the number of iterations needed to perform transformations on the data, thus improving the code's performance and enhancing its readability.

  • Examples: In the following code, we are using a pipeline composed of chained calls toEnum.filter/2 |> Enum.count/1 with the goal of counting how many elements in the original list are multiples of three.

    # Before refactoring:list=Enum.to_list(1..1_000_000)list|>Enum.filter(&(rem(&1,3)==0))|>Enum.count()

    Although this code is correct, it can be refactored by replacing this pipeline with a single call to theEnum.count/2 function, preserving the same behavior as shown below.

    # After refactoring:list=Enum.to_list(1..1_000_000)Enum.count(list,&(rem(&1,3)==0))

    In addition to reducing the code volume, thereby improving readability, the refactored version has better performance than the original, as demonstrated by the benchmarking below conducted with theBenchee library in Elixir.

    Operating System: macOSCPU Information: Intel(R) Core(TM) i7-4578U CPU @ 3.00GHzNumber of Available Cores: 4Available memory: 16 GBElixir 1.14.3Erlang 25.2Benchmark suite executing with the following configuration:warmup: 2 stime: 5 smemory time: 0 nsreduction time: 0 nsparallel: 4inputs: none specifiedEstimated total run time: 14 sBenchmarking original ...Benchmarking refactored ...Name                 ips        average  deviation         median         99th %refactored         28.38       35.24 ms    ±21.72%       33.25 ms       60.06 msoriginal           19.39       51.56 ms    ±47.66%       44.65 ms      171.97 msComparison: refactored         28.38original           19.39 - 1.46x slower +16.32 ms

    The reason for this performance difference is that separate calls the functionsEnum.filter/2 |> Enum.count/1 in a pipeline require two iterations on each transformed data, while it is possible to perform just one iteration on each data with the replacement proposed by the refactoring.

    This same type of refactoring can be applied to the following pipelines:

    • Enum.into/3 is better thanEnum.map/2 |> Enum.into/2
    • Enum.map_join/3 is better thanEnum.map/2 |> Enum.join/2
    • DateTime.utc_now/1 is better thanDateTime.utc_now/0 |> DateTime.truncate/1
    • NaiveDateTime.utc_now/1 is better thanNaiveDateTime.utc_now/0 |> NaiveDateTime.truncate/1
    • OneEnum.map/2 is better thanEnum.map/2 |> Enum.map/2
    • OneEnum.filter/2 is better thanEnum.filter/2 |> Enum.filter/2
    • OneEnum.reject/2 is better thanEnum.reject/2 |> Enum.reject/2
    • OneEnum.filter/2 is better thanEnum.filter/2 |> Enum.reject/2

    These examples are based on code written in Credo's official documentation. Source:link

  • Side-conditions:

    • The pipeline to be replaced must originally consist of chained calls to Elixir's built-in functions;

    • For a pipeline (or part of it) to be replaced by a single function call, there must be built-in Elixir functions that offer behavior equivalent to the set of calls originally chained in the pipeline. Examples of compatible substitutions for Elixir version 1.17.2 are provided above in the documentation of this refactoring. This compatibility may change as the language evolves.

▲ back to Index


Remove single pipe

  • Category: Functional Refactorings.

  • Source: This refactoring emerged from a Grey Literature Review (GLR).

  • Motivation: In Elixir and other languages like F#, pipes (|>) can be used to chain function calls, always passing the result of the previous call as the first parameter to the subsequent one. This feature can help improve the readability of code involving function composition. Although pipes can be very useful for the described purpose, they can be used unnecessarily and excessively, deviating from the intended use of this feature. This refactoring aims to remove pipes that don't involve multiple chained function calls, i.e., those that have only two members, with the first being a variable or a zero-arity function, followed by a function call with arity at least one. These removed pipes, calledsingle pipes, are replaced by a simple call to the function with arity at least one that was its last member, thereby providing cleaner and more readable code.

  • Examples: In the following code, a call to theEnum.reverse/1 function is performed using a single pipe unnecessarily.

    # Before refactoring:list=[1,2,3,4]list|>Enum.reverse()# <-- single pipe!

    To simplify this code, the refactoring will replace the single pipe with a direct call to theEnum.reverse/1 function, preserving the behavior of the original code, as shown below.

    # After refactoring:list=[1,2,3,4]Enum.reverse(list)

    These examples are based on code written in Recode's official documentation. Source:link

  • Side-conditions:

    • To be eligible for refactoring, a pipe must have only two members, with the first being a variable or a zero-arity function call, followed by a function call with arity of at least one.

▲ back to Index


Simplifying pattern matching with nested structs

  • Category: Functional Refactorings.

  • Source: This refactoring emerged from a Grey Literature Review (GLR).

  • Motivation: In Elixir and other functional languages, it is possible to use pattern matching to extract values in a function clause. When using pattern matching to perform deep extraction in nestedstructs, we may create unnecessarily messy and hard-to-understand code. With this refactoring, we can simplify this kind of extraction by performing pattern matching only with the outermoststruct in the nesting, instead of matching patterns with very internalstructs. This transformation improves code readability and reduces its size.

  • Examples: In the following code, the functionfind_favorite_pet/1 takes a%Post{} struct as a parameter and uses pattern matching in the clause to extract the favoritepet of the author from a comment on the post. This value is deeply nested within the existingstruct nesting in the definition of%Post{}.

    # Before refactoring:deffind_favorite_pet(%Post{comment:%Comment{author:%Author{favorite_pet:pet}}})dopetend

    With this refactoring, we can simplify the clause of thefind_favorite_pet/1 function by performing pattern matching only with%Post{}, which is the outermoststruct in the nesting. To access the value of thefavorite_pet, considering that all keys in structs areatoms, we can simply perform a chaining ofstrict access to the nested values, as shown below.

    # After refactoring:deffind_favorite_pet(%Post{}=post)dopost.comment.author.favorite_petend

    This example is based on an original code by David Lucia. Source:link

  • Side-conditions:

    • The deep extraction of a value within nested structs must originally occur in the definition of a function clause;

    • All nestedstructs must have keys defined asatoms;

    • The temporary variable created to perform pattern matching only with the outermoststruct of the nesting must have a name that does not conflict with pre-existing parameters or local variables in the function to be refactored.

▲ back to Index


Improving list appending performance

  • Category: Functional Refactorings.

  • Source: This refactoring emerged from a Grey Literature Review (GLR).

  • Motivation: When we add an element to the end of the list, toensure data immutability, Elixir will duplicate the entire original list, as each of its elements needs to point to a new memory area. Consequently, frequent concatenations at the end of a list can lead to significant memory consumption and hinder performance due to the need to recreate the list many times. With the aim of improving code performance during the concatenation of new elements into a list, this refactoring seeks to replacetail concatenations withhead concatenations, increasing the amount of shared memory between the lists.

  • Examples: In the following code, we are concatenating a new element to the end of a list composed of5_000 elements, which will result in the duplication of the entire original list in memory.

    # Before refactoring:list=Enum.to_list(1..5_000)new_list=list++[new_element]# <-- tail concatenation

    With this refactoring, we simply modify the position where the concatenation of a new element in the list is performed, shifting it to the beginning of the list. This allows for much more memory sharing between the lists and, consequently, improves performance.

    # After refactoring:list=Enum.to_list(1..5_000)new_list=[new_element]++list# <-- head concatenation

    The performance difference between tail and head concatenations can be better visualized by the benchmarking below conducted with theBenchee library in Elixir. Here, we can observe that the refactored version of the code exhibits significantly superior performance.

    Operating System: macOSCPU Information: Intel(R) Core(TM) i7-4578U CPU @ 3.00GHzNumber of Available Cores: 4Available memory: 16 GBElixir 1.14.3Erlang 25.2Benchmark suite executing with the following configuration:warmup: 2 stime: 5 smemory time: 0 nsreduction time: 0 nsparallel: 4inputs: none specifiedEstimated total run time: 14 sBenchmarking head_concatenation ...Benchmarking tail_concatenation ...Name                         ips        average  deviation         median         99th %head_concatenation        5.60 M       0.179 μs ±11846.40%       0.148 μs       0.170 μstail_concatenation      0.0260 M       38.43 μs   ±431.79%       21.57 μs      104.06 μsComparison: head_concatenation        5.60 Mtail_concatenation      0.0260 M - 215.31x slower +38.25 μs
  • Side-conditions: It is important to note that for this refactoring to be applied without altering the code's behavior, the order of elements in the list should not be important for other parts of the system.

    These examples are based on code written in Credo's official documentation. Source:link

▲ back to Index


Convert nested conditionals to pipeline

  • Category: Functional Refactorings.

  • Source: This refactoring emerged from a Grey Literature Review (GLR).

  • Motivation: When conditional statements, such asif..else andcase, are nested to create sequences of function calls, the code can become confusing and have poor readability. In these situations, we can adapt these functions by employingAdd or remove a parameter andIntroduce pattern matching over a parameter. Then we can place the calls to these modified functions in a pipeline using the Elixir pipe operator (|>). This is, therefore, a composite refactoring that has the potential to enhance the readability of code. This refactoring is an alternative to thePipeline using "with".

  • Examples: In the following code, the functionupdate_game_state/3 uses nested conditional statements to control the flow of a sequence of function calls tovalid_move/2,players_turn/2, andplay_turn/3. All these sequentially called functions have a return pattern of{:ok, _} or{:error, _}, which is common in Elixir code.

    # Before refactoring:defpupdate_game_state(%{status::started}=state,index,user_id)do{move,_}=valid_move(state,index)ifmove==:okdoplayers_turn(state,user_id)|>casedo{:ok,marker}->play_turn(state,index,marker)other->otherendelse{:error,:invalid_move}endend

    Note that, although this code works perfectly, the nesting of conditionals used to ensure the safe invocation of the next function in the sequence makes the code confusing. Therefore, we can refactor it by replacing these nested conditional statements with a pipeline using pipe operators (|>), thereby reducing the number of lines of code and improving readability.

    # After refactoring:defpupdate_game_state(%{status::started}=state,index,user_id)dostate|>valid_move(index)|>players_turn(state,user_id)|>play_turn(state,index,marker)end
  • Side-conditions: It is important to note that for this refactoring to be applied without altering the code's behavior, some functions in the pipeline had to have their signatures changed. Specifically,players_turn/2 andplay_turn/3 becameplayers_turn/3 andplay_turn/4 respectively. The additional parameter in each of these functions is meant to receive the returns of the previous functions in the pipeline, which are in the patterns{:ok, _} or{:error, _}, and then guide their internal flows.

    This example is based on an original code by Gary Rennie. Source:link

▲ back to Index


Replacing recursion with a higher-level construct

  • Category: Functional Refactorings.

  • Source: This refactoring emerged from a Grey Literature Review (GLR).

  • Motivation: When we read code that uses recursion, it's easy to focus primarily on its mechanics, in other words, the correction of recursion and whether it will pass every test thrown at it. However, due to the level of abstraction that recursive code can have, it can become less expressive, diverting the developer's focus from what should be more important: what the algorithm does and how it does it. This can occur due to the cognitive load required for understanding recursive code, especially when it was developed by someone else. Elixir, like other functional languages, provides many higher-order functions that enable iterations while hiding the details of recursion. This refactoring transforms recursive functions into calls to higher-order functions, making the code less verbose and more maintainable.

  • Examples: In the following code, the moduleFoo has two recursive functions,factorial/1 andsum_list/1. Both use recursion to perform iterations.

    # Before refactoring:defmoduleFoododeffactorial(0),do:1deffactorial(n),do:n*factorial(n-1)defsum_list([]),do:0defsum_list([head|tail])dohead+sum_list(tail)endend

    As shown in the following code, we can refactor these functions by replacing recursion with simple calls toEnum.reduce/3, which is a higher-order function. In addition to preserving their behavior, the refactored code becomes more concise and readable.

    # After refactoring:defmoduleFoododeffactorial(n)doEnum.reduce(1..n,1,&(&1*&2))enddefsum_list(list)doEnum.reduce(list,0,&(&1+&2))endend

    Although this example used theEnum.reduce/3 function in the refactoring, Elixir has various other built-in higher-order functions that could be used to refactor code with different behaviors than those presented in this example.

  • Side-conditions:

    • For recursive functions to be eligible for refactoring, they must exhibit behavior identical to the function calls defined in theEnum module of Elixir. In other words, even though the bodies of the originally recursive functions are replaced with built-in function calls from theEnum module, no caller of the original functions should be altered, and all existing tests in the codebase should continue to behave in the same way.

▲ back to Index


Replace a nested conditional in a "case" statement with guards

  • Category: Functional Refactorings.

  • Source: This refactoring emerged from a Mining Software Repositories (MSR) study.

  • Motivation: Thecase statements allow us to compare an expression against many different patterns until we find one that matches. When more complex checks need to be performed withincase statements, it's possible to use other conditional instructions likeif..else nested inside acase. This refactoring aims to replace nested conditional statements within acase with the use of guards, maintaining the ability to perform more complex pattern matching checks while improving code readability.

  • Examples: In the following code, the moduleFile.Stream has a functionreduce/3 that uses a nestedif..else statement within acase to perform a more complex pattern matching check.

    # Before refactoring:defmoduleFile.Streamdodefreduce(%{path:path,modes:modes},acc,fun)dostart_fun=fn->case:file.open(path,read_modes(modes))do{:ok,device}->if:strip_bominmodes,do:strip_bom(device),else:device{:error,reason}->raise(File.Error,reason)endend...end...end

    As shown in the following code, we can refactor this function by replacing the nestedif..else conditional within thecase statement with a guard clause. Not only does this preserve the behavior, but it also makes the refactored code more concise and readable.

    # After refactoring:defmoduleFile.Streamdodefreduce(%{path:path,modes:modes},acc,fun)dostrip_bom?=:strip_bominmodes#<- "Extract Expressions" refactoring!start_fun=fn->case:file.open(path,read_modes(modes))do{:ok,device}whenstrip_bom?->strip_bom(device)#<- Guard replacing nested conditional!{:ok,device}->device{:error,reason}->raise(File.Error,reason)endend...end...end

    Note that in this example, we performed a composite refactoring. In order to facilitate the replacement of the nested conditional command within thecase, we also performed theExtract expressions refactoring to create the local variablestrip_bom?.

    This example is based on an original code by Andrea Leopardi. Source:link

  • Side-conditions:

    • The guardwhen should be used in thecase clause that originally had a nestedif..else to replace the branch equivalent to theif;

    • On the other hand, a clause with pattern matching identical to the one originally used in the clause that had nested conditionals should be added immediately after the one described in the previous condition. Its purpose is to replace the branch originally defined by theelse.

▲ back to Index


Replace function call with raw value in a pipeline start

  • Category: Functional Refactorings.

  • Source: This refactoring emerged from a Mining Software Repositories (MSR) study.

  • Motivation: In Elixir and other functional languages such as F#, the pipe operator (|>) facilitates chaining function calls, consistently passing the return of one call as the initial parameter of the next. This functionality enhances the clarity of code employing function composition. While pipes can commence with a function call, they are often more readable when initiated with araw value. This refactoring targets to change the beginning of a pipeline, extracting the initial parameter from the function call that originally starts the pipe and incorporating this value at the pipeline's start.

  • Examples: In the following code, the moduleFoo has a functionbar/1 that takes a list as a parameter, doubles all the values ​​in the list, and then returns the smallest of them. Disregard any performance issues that this code may have and focus solely on the format of the function pipeline used to perform this operation. Before being refactored, this pipeline starts with a call to theEnum.map/2 function instead of araw value, which can make it less readable.

    # Before refactoring:defmoduleFoododefbar(list)doEnum.map(list,&(&1*2))|>Enum.sort|>Enum.at(0)endend#...Use example...iex(1)>Foo.bar([10,6,90,8,3,9])6

    As demonstrated in the following code, we can refactor this function by extractinglist, the first parameter ofEnum.map/2, and placing thisraw value at the beginning of the pipeline. Although the refactored code has one more line than its previous version, it is more readable because it makes it clearer which value will undergo a series of sequential operations in the pipeline.

    # After refactoring:defmoduleFoododefbar(list)dolist#<- Raw value!|>Enum.map(&(&1*2))|>Enum.sort|>Enum.at(0)endend#...Use example...iex(1)>Foo.bar([10,6,90,8,3,9])6
  • Side-conditions:

    • To be refactored, the pipeline must start with the call of a function with an arity of one or more. Additionally, the first parameter of the function call that originally starts the pipeline must be provided by a variable.

▲ back to Index


Erlang-Specific Refactorings

Erlang-specific refactorings are those that use programming features unique to the Erlang ecosystem (e.g., OTP, typespecs, and behaviours). In this section, 11 different refactorings classified as Erlang-specific are explained and exemplified:

Typing parameters and return values

  • Category: Erlang-specific Refactorings.

  • Note: Formerly known as "Generate function specification".

  • Motivation: Despite being a dynamically-typed language, Elixir offers a feature to compensate for the lack of a static type system. By usingTypespecs, we can specify the types of each function parameter and of the return value. Utilizing this Elixir feature not only improves documentation, but also can enhance code readability and prepare it to be analyzed for tools likeDialyzer, enabling the detection of type inconsistencies, and potential bugs. The goal of this refactoring is simply to useTypespecs in a function to promote the aforementioned benefits of using this feature.

  • Examples: The following code has already been presented in another context in the refactoringExtract expressions. Prior to the refactoring, we have a moduleBhaskara composed of the functionsolve/3, responsible for finding the roots of a quadratic equation. Note that this function should receive three real numbers as parameters and return a tuple of two elements. The first element of this tuple is always an atom, while the second element may be a String (if there are no roots) or a tuple containing the two roots of the quadratic equation.

    # Before refactoring:defmoduleBhaskaradodefsolve(a,b,c)dodelta=(b*b-4*a*c)ifdelta<0do{:error,"No real roots"}elsex1=(-b+delta**0.5)/(2*a)x2=(-b-delta**0.5)/(2*a){:ok,{x1,x2}}endendend#...Use examples...iex(1)>Bhaskara.solve(1,3,-4){:ok,{1.0,-4.0}}iex(2)>Bhaskara.solve(1,2,3){:error,"No real roots"}

    To easier this code understanding and leverage the other aforementioned benefits, we can generate a function specification using the@spec module attribute which is a default feature of Elixir. This module attribute should be placed immediately before the function definition, following the pattern@spec function_name(arg_type, arg_type...) :: return_type.

    # After refactoring:defmoduleBhaskarado@specsolve(number,number,number)::{atom,String.t()|{number,number}}defsolve(a,b,c)dodelta=(b*b-4*a*c)ifdelta<0do{:error,"No real roots"}elsex1=(-b+delta**0.5)/(2*a)x2=(-b-delta**0.5)/(2*a){:ok,{x1,x2}}endendend#...Retrieving code documentation...iex(1)>hBhaskara.solve/3@specsolve(number(),number(),number())::{atom(),String.t()|{number(),number()}}

    Note that with the use of@spec, we can easily check the function specification using Elixir's helper.

  • Side-conditions:

    • This refactoring is free from any preconditions or postconditions, and can therefore always be used to improve the specification of any named function.

▲ back to Index


Moving error-handling mechanisms to supervision trees

  • Category: Erlang-specific Refactorings.

  • Note: Formerly known as "From defensive to non-defensive programming style".

  • Motivation: This refactoring helps to transform defensive-style error-handling code written in Elixir into supervised processes. This non-defensive style, also known as "Let it crash style", isolates error-handling code from business rule code in a system. When a process is supervised in a tree, it doesn't need to worry about error handling because if errors occur, its respective supervisor will monitor and restart it.

  • Examples: The following code shows an example of this refactoring. Before the refactoring, we have aGenServer process responsible for keeping a numerical counter. Note that it uses the defensive style (try..rescue) in the callback responsible for thebump/2 function. Therefore, if a non-numerical value is provided to this function, instead of a crash, the code will simply keep the counter in its current state.

    # Before refactoring:defmoduleCounterdouseGenServer...defbump(value,pid_name\\__MODULE__)doGenServer.call(pid_name,{:bump,value})get(pid_name)end## Callbacks...@impltruedefhandle_call({:bump,value},_from,counter)dotrydo{:reply,counter,counter+value}rescue_einArithmeticError->{:reply,counter,counter}endendend#...Use examples...iex(1)>Counter.start(15,C2){:ok,#PID<0.120.0>}iex(2)>Counter.get(C2)15iex(3)>Counter.bump(-3,C2)12iex(4)>Counter.bump("Jose",C2)12#<= unchanged counter!

    To maintain this same code behavior without usingtry..rescue error-handling mechanisms, we can turnCounter into a supervised process in a tree, as presented in thiscode. Therefore, if a string is provided tobump/2, the process will crash but will be restarted by its supervisor with their last state.

▲ back to Index


From meta to normal function application

  • Category: Erlang-specific Refactorings.

  • Motivation: The functionapply/3 provided by the Elixir Kernel allows calling any function that has its source module, name, and parameter list defined at runtime. This refactoring allows replacing the use of theapply/3 function with direct calls to functions that have modules, names, and parameter lists defined at compile time.

  • Examples: The following code shows an example of this refactoring.

    # Before refactoring:iex(1)>apply(Enum,:sort,[[4,3,2,1]])[1,2,3,4]
    # After refactoring:iex(1)>Enum.sort([4,3,2,1])[1,2,3,4]
  • Side-conditions:

    • In the original code, the definition of which function will be called byapply/3 and its respective parameters should occur statically, meaning at compile time.

▲ back to Index


Remove unnecessary calls to length/1

  • Category: Erlang-specific Refactorings.

  • Motivation: In Elixir, lists are always linked. Therefore, the cost of eachlength/1 function call is not constant but proportional to the size of the list passed as a parameter. Considering that this cost can be high, manylength/1 calls can be unnecessary, making the code inefficient. This refactoring aims to replace these unnecessary calls with pattern matching, improving the efficiency of the code without modifying its behavior.

  • Examples: The following code shows an example of this refactoring. Consider a functionfoo/1 that useslength/1 in a guard clause to check if a list is empty. Thislength/1 call is inefficient and unnecessary for very large lists.

    # Before refactoring:defmoduleBardodeffoo(list)whenlength(list)==0do:empty_listenddeffoo(list)whenlength(list)!=0do:non_empty_listendend#...Use examples...iex(1)>Enum.to_list(1..1_000_000)|>Bar.foo():non_empty_list

    This refactoring can replace the use oflength/1 with pattern matching, resulting in a more efficient code with the same behavior, as shown below.

    # After refactoring:defmoduleBardodeffoo([])do:empty_listenddeffoo([_|_])do:non_empty_listendend#...Use examples...iex(1)>Enum.to_list(1..1_000_000)|>Bar.foo():non_empty_list
  • Side-conditions:

    • The function to be refactored should receive alist as a parameter and call thelength/1 function in guard clauses to check whether thelist is empty or not.

▲ back to Index


Add type declarations and contracts

  • Category: Erlang-specific Refactorings.

  • Motivation: Despite being a dynamically-typed language, Elixir offers a feature to compensate for the lack of a static type system. By usingTypespecs, we can specify the types of each function parameter and of the return value. Utilizing this Elixir feature not only improves documentation, but also can enhance code readability and prepare it to be analyzed for tools likeDialyzer, enabling the detection of type inconsistencies, and potential bugs. The goal of this refactoring is to useTypespecs to create custom data types, thereby naming recurring data structures in the codebase and increasing system readability.

  • Examples: The following code examples illustrate this refactoring. Prior to refactoring, we have a functionset_background/1 that receives a tuple of three integer elements. This function performs some processing with this tuple and returns an atom. The function interface forset_background/1 is defined in the module attribute@spec.

    # Before refactoring:defmoduleFoodo@specset_background({integer,integer,integer})::atomdefset_background(rgb)do#do something...:okendend#...Use examples...iex(1)>Foo.set_background({150,25,89}):ok

    To easier this code understanding and leverage the other aforementioned benefits, we can generate a type specification using the@type module attribute which is a default feature of Elixir.

    # After refactoring:defmoduleFoodo@typedoc"""    A tuple with three integer elements between 0..255  """@typecolor::{red::integer,green::integer,blue::integer}@specset_background(color)::atomdefset_background(rgb)do#do something...:okendend#...Use examples...iex(1)>Foo.set_background({150,25,89}):ok#...Retrieving code documentation...iex(2)>hFoo.set_background/1@specset_background(color())::atom()iex(3)>tFoo.color#<= type documentation!@typecolor()::{red::integer(),green::integer(),blue::integer()}A tuplewiththreeintegerelementsbetween0..255

    Note that with the use of@type, we can easily check the type specification using Elixir's helper.

  • Side-conditions:

    • The name of the type created by this refactoring (e.g.,color()) must be unique. In other words, it must be different from all predefined basic types in Elixir (e.g.,integer(),float(),atom(), etc.), as well as from all custom data types defined in the refactored module or any other module in the codebase, including those from imported external libraries.

    • Although type names and function names do not technically conflict, having a type with the same name as a function defined in the module can cause confusion for code readers. Therefore, it is a good practice to ensure that type names are unique, even in relation to function names. For this reason, this is also a side condition of this refactoring.

▲ back to Index


Introduce processes

  • Category: Erlang-Specific Refactorings.

  • Note: Formerly known as "Introduce/remove concurrency".

  • Motivation: This refactoring involves introducing concurrent processes to achieve a more optimal mapping between parallel processes and parallel activities of the problem being solved. This eliminates bottlenecks by a better code design, enabling greater scalability and better performance.

  • Examples: An example of using thisrefactoring to introduce concurrency can be seen in the following code.Todo.Database is aGenServer process that is part of a concurrent system. As can be seen in the implementation of itsstart/0 function, it is a singleton, meaning there is only oneTodo.Database process in the entire system. Since this process is responsible for providing access to the system's database for all its N different clients, bottlenecks or other issues can naturally occur. Imagine a situation where the number of calls to thestore/2 function is very large, to the point where this singleTodo.Database process cannot handle the previousstore/2 call before the subsequent calls arrive for the same function. This could cause an overload of theTodo.Database mailbox, resulting in excessive memory usage and ultimately an overflow of the BEAM OS process where this system is executed.

    # Before refactoring:defmoduleTodo.DatabasedouseGenServer...defstartdoGenServer.start(__MODULE__,nil,name:__MODULE__)#<-- Singleton process!enddefstore(key,data)doGenServer.cast(__MODULE__,{:store,key,data})enddefhandle_cast({:store,key,data},state)dokey|>file_name()|>File.write!(:erlang.term_to_binary(data)){:noreply,state}end...end

    To avoid this bottleneck inTodo.Database, we can refactor the callback functionhandle_cast/2, introducing concurrency by a newTask process that will be responsible for handling calls to thestore/2 function.

    # After refactoring:defmoduleTodo.DatabasedouseGenServer...defhandle_cast({:store,key,data},state)doTask.start(fn->#<-- Concurrency Introduced!key|>file_name()|>File.write!(:erlang.term_to_binary(data))end){:noreply,state}end...end

    AlthoughTodo.Database continues to be a singleton process, with this refactoring, each call to thestore/2 function will be handled by a different process introduced inhandle_cast/2, allowing for greater scalability with multiple worker processes executing concurrently.

    This example is based on an original code by Saša Jurić available in the"Elixir in Action, 2. ed." book, where another possibility to introduce concurrency by using a pool of processes is also presented.

▲ back to Index


Remove processes

  • Category: Erlang-Specific Refactorings.

  • Note: Formerly known as "Introduce/remove concurrency".

  • Motivation: This refactoring involves removing unnecessary concurrent processes and replacing them with Elixir regular modules. When processes are used to perform tasks that could be handled by regular modules, aside from compromising code readability, they can lead to excessive memory consumption and system bottlenecks due to the accumulation of unprocessed messages in the mailbox. Therefore, in addition to improving the code design and consequently its readability, this refactoring can enhance the overall performance of a system.

  • Examples: An example of using thisrefactoring to remove concurrency can be seen in eliminating the code smellCode organization by process. Here, we have a code that previously used processes, callbacks, and message passing where a simpler regular module and a function call would have enough. This refactoring allowed for the improvement of code quality without altering its behavior.

  • Side-conditions:

    • To perform this refactoring, it is necessary to be able to replace process callbacks with conventional function calls without making changes to the public interfaces of the refactored module.

▲ back to Index


Add a tag to messages

  • Category: Erlang-Specific Refactorings.

  • Motivation: In Elixir, processes run in an isolated manner, often concurrently with others. Communication between different processes is performed through message passing. This refactoring aims to adapt processes that communicate with each other by adding tags that identify groups of messages exchanged between them. This identification allows for different treatments of received messages based on their purpose or format.

  • Examples: The following code examples illustrate this refactoring. Prior to the refactoring, the modulesReceiver andSender, which will generate distinct processes, communicate via message exchange. More specifically, the process whereSender is located sends a message that is received by theReceiver process, which in turn simply displays the message, regardless of its format.

    # Before refactoring:defmoduleReceiverdo@doc"""    Function for receiving messages from processes.  """defrun()doreceivedomsg_received->IO.puts("Message:#{msg_received}")after30_000->IO.puts("Timeout...")endend@doc"""    Create a process to receive a message.    Messages are received in the run() function of Receiver.  """defcreate()dospawn(Receiver,:run,[])endend
    # Before refactoring:defmoduleSenderdo@doc"""    Function for sending messages between processes.      pid_receiver: message recipient      msg: messages of any type and size can be sent.  """defsend_msg(pid_receiver,msg)dosend(pid_receiver,msg)endend#...Use examples...iex(1)>pid=Receiver.create()#PID<0.320.0>iex(2)>Sender.send_msg(pid,"Hello World!")Message: HelloWorld!

    Following the refactoring,Sender.send_msg/2 has been transformed intoSender.send_msg/3. Its additional parameter is responsible for receiving thetag that will identify the sent message. However, this parameter has a default value (:msg) set, so all pre-existing calls toSender.send_msg/2 will have their behavior preserved after the refactoring.

    # After refactoring:defmoduleReceiverdo@doc"""    Function for receiving messages from processes.  """defrun()doreceivedo{:msg,msg_received}->IO.puts("Message:#{msg_received}"){:sum,{v1,v2}}->IO.puts("Result:#{v1+v2}"){_,_}->IO.puts("Won't match!")after30_000->IO.puts("Timeout...")endend@doc"""    Create a process to receive a message.    Messages are received in the run() function of Receiver.  """defcreate()dospawn(Receiver,:run,[])endend
    # After refactoring:defmoduleSenderdo@doc"""  Function for sending messages between processes.    pid_receiver: message recipient    msg: messages of any type and size can be sent.    tag: used by receiver to decide what to do            when a message arrives.            Default is the atom :msg  """defsend_msg(pid_receiver,msg,tag\\:msg)dosend(pid_receiver,{tag,msg})endend#...Use examples...iex(1)>pid=Receiver.create()#PID<0.320.0>iex(2)>Sender.send_msg(pid,"Hello World!")Message: HelloWorld!iex(3)>Sender.send_msg(pid,{1,2},:sum)Result:3iex(4)>Sender.send_msg(pid,msg,:test)Won't match!

    Note that all messages sent between these processes have the format of a tuple{tag, msg} after the refactoring. In addition, the functionReceiver.run/0 now uses pattern matching to provide different treatments for messages identified with different tags. The programmer has the freedom to adaptReceiver.run/0 by configuring all message identification tags relevant to their system.

  • Side-conditions:

    • The function whose arity is modified in this refactoring (e.g.,Sender.send_msg/3) should not conflict in name and arity with other functions that are already predefined or imported by the refactored module.

▲ back to Index


Register a process

  • Category: Erlang-Specific Refactorings.

  • Motivation: In Elixir, processes run in an isolated manner, often concurrently with others. Communication between different processes is performed through message passing. This refactoring involves assigning a user-defined name to a process ID and using that user-defined name instead of the process ID in message passing. Any process in an Elixir system can communicate with a registered process even without knowing its ID.

  • Examples: The following code examples illustrate this refactoring. The modulesReceiver andSender used here are defined in the examples ofAdd a tag to messages. Prior to the refactoring, for a process to send a message specifically to the process of theReceiver module, it would need to know its identifier (#PID<0.320.0>).

    # Before refactoring:iex(1)>pid=Receiver.create()#PID<0.320.0>iex(2)>Sender.send_msg(pid,"Hello World!")Message: HelloWorld!

    Following the refactoring, the process with the identifier#PID<0.320.0> was registered with the user-defined name:receiver. This enables more readable code and allows any other process in the system to communicate with this registered process using only its name.

    # After refactoring:iex(1)>pid=Receiver.create()#PID<0.320.0>iex(2)>Process.register(pid,:receiver)iex(3)>Sender.send_msg(:receiver,"Hello World!")Message: HelloWorld!
  • Side-conditions:

    • The process name specified by the user must be anatom and not already assigned as a process name within the program in question;

    • The chosen process for registration must not have been previously registered;

    • The process to be registered must not have multiple instances coexisting simultaneously.

    These side conditions are based on definitions written by Huiqing Liet al. in this paper:[1]

▲ back to Index


Behaviour extraction

  • Category: Erlang-specific Refactorings.

  • Motivation: This refactoring is similar to Extract Interface, proposed by Fowler and Beck for object-oriented languages. In Elixir, abehaviour serves as an interface, which is a contract that a module can fulfill by implementing functions in a guided way according to the formats of parameters and return types defined in the contract. Abehaviour is an abstraction that defines only the functionality to be implemented, but not how that functionality is implemented. When we find a function that can be repeated in different modules but performs special roles in each of them, it can be a good idea to abstract this function by extracting it to abehaviour, standardizing a contract to be followed by all modules that implement or may implement it in the future.

  • Examples: The following code example illustrates the use of this refactoring technique. In this case, the moduleFoo has two functions. The functionprint_result/2 has a generic behavior, that is, it simply displays the result of an operation. On the other hand, the functionmath_operation/2 has a special role in this module, which is to attempt to sum two numbers and return a tuple that may have the operation's result or an error if invalid parameters are passed to the function call.

    # Before refactoring:defmoduleFoododefmath_operation(a,b)whenis_number(a)andis_number(b)do{:ok,a+b}enddefmath_operation(_,_),do:{:error,"args not numeric"}defprint_result(a,b)do{_,r}=math_operation(a,b)IO.puts("Operation result:#{r}")endend#...Use examples...iex(1)>Foo.math_operation(1,2){:ok,3}iex(2)>Foo.math_operation(1,"jose"){:error,"args not numeric"}iex(3)>Foo.print_result(1,2)Operationresults:3

    Although this is a simple example, note thatmath_operation/2 could eventually be implemented in other modules to perform different special roles, such as division, multiplication, subtraction, etc. With that in mind, we can standardize a contract formath_operation/2, guiding developers to follow the same format every time this function is implemented in the codebase. To do so, this refactoring will transformFoo into a behaviour definition by creating a@callback that defines the format ofmath_operation/2. In addition, usingMoving a definition, we will movemath_operation/2 to a new module calledSum, updating all previous calls tomath_operation/2. Finally,Sum should explicitly implement the contract defined byFoo using the@behaviour definition.

    # After refactoring:defmoduleFoodo# behaviour definition@callbackmath_operation(a::any(),b::any())::{atom(),any()}defprint_result(a,b)do{_,r}=Sum.math_operation(a,b)# <- new refactoring opportunity!IO.puts("Operation result:#{r}")endend#...Use examples...iex(1)>Foo.print_result(1,2)Operationresult:3
    # After refactoring:defmoduleSumdo@behaviourFoo#<- behaviour implementation@implFoo#<- behaviour implementationdefmath_operation(a,b)whenis_number(a)andis_number(b)do{:ok,a+b}enddefmath_operation(_,_),do:{:error,"args not numeric"}end#...Use examples...iex(1)>Sum.math_operation(1,2){:ok,3}iex(2)>Sum.math_operation(1,"Jose"){:error,"args not numeric"}

    After this refactoring, the moduleFoo acts as thebehaviour definition and the moduleSum as thebehaviour instance. This refactoring is highly valuable since behaviour constructs allow static code analysis tools such asDialyzer to have a better understanding of the code, offer useful recommendations, and detect potential issues.

    Recalling previous refactorings: Although this refactoring was successfully completed, note that it created a new opportunity for refactoring in the functionFoo.print_result/2. The first line of this function remained with a hard-coded call toSum.math_operation/2, which is an implementation of the@callback defined in the behaviour. Imagine that in the future the moduleSubtraction, which also implements theFoo behaviour, is created:

    defmoduleSubtractiondo@behaviourFoo#<- behaviour implementation@implFoo#<- behaviour implementationdefmath_operation(a,b)whenis_number(a)andis_number(b)do{:ok,a-b}enddefmath_operation(_,_),do:{:error,"args not numeric"}end

    To makeFoo.print_result/2 able to display the results of any possible implementation of theFoo behaviour (e.g.Sum andSubtraction), we can applyGeneralise a function definition to it, resulting in the following code:

    defmoduleFoodo@callbackmath_operation(a::any(),b::any())::{atom(),any()}defprint_result(a,b,math_operation)do{_,r}=math_operation.(a,b)#<- generalised!IO.puts("Operation result:#{r}")endend#...Use examples...iex(1)>Foo.print_result(1,2,&Sum.math_operation/2)Operationresult:3iex(2)>Foo.print_result(1,2,&Subtraction.math_operation/2)Operationresult:-1

    These examples are based on Erlang code written in this paper:[1]

  • Side-conditions:

    • The module that defines the callbacks for the behaviour after the refactoring (e.g.,Foo) should not originally have callbacks with the same name, or even functions defined or imported previously with the same name as the callbacks defined in the refactoring (e.g.,math_operation/2), thus avoiding conflicts.

    • The module that implements the behaviour defined in the refactoring (e.g.,Sum) should not originally have functions defined or imported that conflict in name and arity with the callbacks from the behaviour definition.

▲ back to Index


Behaviour inlining

  • Category: Erlang-specific Refactorings.

  • Motivation: This refactoring is the inverse ofBehaviour extraction. Remembering, behaviour extraction aims to define a callback to compose a standardized interface for a function in a module that acts as abehaviour definition and move the existing version of that function to another module that follows this standardization, implementing the callback (behaviour instance). In contrast, Behaviour inlining aims to eliminate the implementations of callbacks in abehaviour instance.

  • Examples: To perform this elimination, the function that implements a callback in abehaviour instance is moved to thebehaviour definition module usingMoving a definition, which will handle possible naming conflicts and update references to that function. If the moved function was the only callback implemented by thebehaviour instance module, the definition of the implemented behaviour (@behaviour) should be removed from thebehaviour instance, thus turning it into a regular module. Additionally, when the moved function is the last existing implementation of the callback throughout the codebase, this callback should cease to exist, being removed from thebehaviour definition module.

    To better understand, take a look at the example inBehaviour extraction in reverse order, that is,# After refactoring: -># Before refactoring:.

  • Side-conditions:

    • The module that originally defines the callbacks for the behaviour should not have functions defined or imported previously with the same name as the function moved to it in this refactoring, thus avoiding conflicts;

    • To avoid breaking changes, the definition of the behaviour callbacks can only be removed by this refactoring if no other module implements the same behaviour in the codebase.

▲ back to Index


About

This catalog was proposed by Lucas Vegi and Marco Tulio Valente, fromASERG/DCC/UFMG.

For more info see the following paper:

Please feel free to make pull requests and suggestions (Issues tab).

▲ back to Index

Acknowledgments

Our research is part of the initiative calledResearch with Elixir (in portuguese). We are supported byDashbit andRebase, which are companies that support this initiative:





We were also supported byFinbits, a Brazilian Elixir-based fintech that is a supporter of this initiative:



▲ back to Index

Footnotes

  1. This refactoring emerged from a Grey Literature Review (GLR).23456789101112131415161718192021

  2. This refactoring emerged from a Mining Software Repositories (MSR) study.2345

  3. This refactoring emerged from an extended Systematic Literature Review (SLR).


[8]ページ先頭

©2009-2025 Movatter.jp