Skip to content

Instantly share code, notes, and snippets.

@nicmarti
Last active March 5, 2024 20:54
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nicmarti/b8dd7ecabf09f7b0297559adc1d5bb26 to your computer and use it in GitHub Desktop.
Save nicmarti/b8dd7ecabf09f7b0297559adc1d5bb26 to your computer and use it in GitHub Desktop.
Exercice Scala niveau débutant

Lunatech

Scala tutorial.

Audience : cet exercice s'adresse à des développeurs ayant quelques bases en Scala. Il permet de découvrir la mise en place progressive d'un ensemble de fonctions et de types, pour résoudre un problème simple.

L'objectif de cet exercice est de réaliser un générateur d'équipement pour le jeu Minecraft. Minecraft est un jeu d'exploration. L'aventurier doit trouver des ingrédients de base, puis les combiner afin de constuire de l'équipement. Nous allons jouer une version simplifiée.

A partir d'une certaine quantité d'ingrédients, par exemple 5 blocs de bois, votre moteur doit trouver quelles sont les recettes les plus efficaces pour laisser le moins d'ingrédient non utilisé. Une épée demandera 2 éléments. Une Hache demandera 3 éléments. Enfin une pioche ne demandera qu'un élément Avec 5 blocs de bois, pour ne pas gaspiller d'éléments, nous pouvons donc fabriquer une hache (3 blocs) et une épée (2 blocs).

Votre mission est donc de créer un moteur simple capable d'exécuter des Recettes (Recipe) en utilisant les matériaux.

Vous serez guidé pas à pas. Un ensemble de tests unitaires sera utilisé à partir de l'étape 5.

Si cet exercice vous a intéressé, et que vous souhaitez coder en Scala, sachez que Lunatech recrute. Rendez-vous sur notre site web http://www.lunatech.fr dans la rubrique Emploi.

Etape 1

Dans un premier temps nous allons voir comment créer des Enumerations en Scala.

Créer un fichier WeaponGenerator.scala dans lequel vous devrez définir 2 Enumerations.

D'une part une Enumeration de Material avec DIAMOND, IRON et WOOD D'autre part une Enumeration de ToolType avec AXE, SWORD et SHOVEL

Regardez dans la documentation de Scala comment créer des Enumerations.

Ensuite, ajoutez une case class Weapon composée d'un ToolType et d'un Material. Par exemple une épée en bois sera créée de cette façon

Wooden Sword is Weapon(ToolType.SWORD, Material.WOOD)

Enfin, créer une case class Ingredient, pour représenter une certaine quantité de Material.

A la fin de cette étape vous devez avoir :

  • deux Enumeration
  • une case class Weapon
  • une case class Ingredient avec un attribut quantity de type Int et un material

Etape 2

Dans cette deuxième étape, nous commencerons par créer un object MinecraftToolsGenerator. Ce singleton nous servira pour porter 3 types importants pour la suite. Pour simplifier l'algorithme, notre constructeur d'outil fonctionnera à partir d'un Ingredient. Par exemple

Ingredient(6, WOOD) // 6 éléments en bois

Commençons par définir un type pour représenter une arme et éventuellement une liste d'ingrédient, qui seront les restes après avoir fabriqué une arme :

  type WeaponAndIngredients = (Weapon, Option[Ingredient])

Nous continuons ensuite en créant une recette (Recipe). Ce type est un alias pour une fonction qui transforme un Ingredient en WeaponAndIngredients

  type Recipe = Ingredient => WeaponAndIngredients

Enfin, nous aurons nos formules de fabrication. Celles-ci prennent un Ingredient (par exemple 3 éléments d'Acier) et retournerons soit une arme avec le reste des ingrédients non utilisés, soit un message d'erreur, représenté par une String.

Regardez la documentation de la class Either. Celle-ci permet de représenter le fait qu'une fonction peut retourner soit un type de résultat (ici un String) soit un autre type (WeaponAndIngredients) Mais pas les deux. Un Either peut être soit un Left, soit un Right.

  type RecipeBuilder = Ingredient => Either[String, WeaponAndIngredients]

Etape 3

Nous allons maintenant commencer à construire la structure de notre logiciel. Pour cela, dans un premier temps, nous vous proposons de créer une fonction coreRecipe avec la signature suivante :

   private def coreRecipe(ingredient: Ingredient, t: ToolType, requiredQuantity: Int): (Weapon, Option[Ingredient]) = { 
     ???
   }

A vous d'implémenter la fonction coreRecipe. Si vous n'arrivez pas à implémenter cette fonction, continuez l'exercice pour l'instant.

Etape 4

Vous devez maintenant définir 3 recettes de fabrication. Une pour chaque type d'arme que nous pouvons fabriquer. D'après le guide de Minecraft, une épée (sword) demande 2 Ingredient du même type pour être construite, une Hache (axe) demande 3 Ingredient du même type et enfin une pioche (shovel) ne demande qu'une quantité unitaire. Avec Ingredient(2, WOOD) je peux donc construire une ,épée, et il ne me restera plus d'ingrédients. Je pourrai aussi construire une pioche et il me restera une quantité de 1 de WOOD. Par contre, je ne pourrai pas construire de hache. Et c'est justement là que notre RecipeBuilder nous indiquera qu'il nous manque des ingrédients, sous la forme d'une String.

A vous maintenant de coder les 3 Recipe, en faisant appel uniquement à coreRecipe =>

 def swordRecipe: Recipe = ???
 def axeRecipe: Recipe = ???
 def shovelRecipe: Recipe = ???

Nous avons maintenant nos 3 recettes basées sur coreRecipe.

Etape 5

Nous allons maintenant définir 3 fonctions qui nous permettrons de construire nos armes. D'après notre système de type, il faudra définir 3 fonctions qui retournent un RecipeBuilder

Ajouter les 3 fonctions suivantes et complétez pour chaque fonction ce qu'il manque.

!!! Utilisez les tests unitaires de la class TestMinecraftToolsGenerator afin de valider les contrats de vos fonctions

  def buildSword: RecipeBuilder = {
   ???
 }
 
 def buildAxe: RecipeBuilder = {
  ???
 }
 
 def buildShovel: RecipeBuilder = {
  ???
 } 
 

Si vous écrivez les 3 fonctions, vous remarquerez que du code se répète dans les 3 fonctions.

Remplacez le code dupliqué par une fonction craft.

Si vous êtes bloqué, la fonction craft doit avoir cette signature

def craft(recipe: Recipe, ingredient: Ingredient): Either[String, WeaponAndIngredients] = 

Etape 6

Avant-dernière étape. Nous vous demandons de définir une fonction uncraft, qui permettra de découvrir la quantité d'éléments nécessaires pour chaque type de Weapon.

La signature de la fonction sera de ce type :

  def uncraft: Weapon => Ingredient = {
  }

Utilisez les tests unitaires pour valider votre implémentation

Etape 7

C'est maintenant que le plus important commence. Votre dernière mission est d'écrire la fonction buildBestPackageFrom, et de faire passer tous les tests unitaires de la class qui accompagne l'exercice

def buildBestPackageFrom(ingredients: Ingredient): Set[Weapon] = {

}

Cette fonction prend en argument un Ingredient, et retourne la meilleur combinaison d'outils possible. Par "meilleur combinaison" on entend celle qui est la plus écologique, et qui laisse le moins d'ingrédients à la fin

A titre d'exemple, si vous passez un Ingredient avec 5 WOOD alors nous aimerions que votre code construise une Hache (3 blocs de bois) et une épée (2 blocs de bois)


    "it has  5 Wood block" should {
      "build 1 Axe and 1 axe" in {
        // GIVEN
        val ingredients = Ingredient(5, Material.IRON)

        val expected = Set(Weapon(ToolType.SWORD, Material.IRON),
          Weapon(ToolType.AXE, Material.IRON)
        )

        MinecraftToolsGenerator.buildBestPackageFrom(ingredients) should be(expected)
      }
    }

C'est terminé. Vous venez de voir comment modéliser un petit algorithme simple, en utilisant le système de type de Scala. On remarque que chaque traitement est décomposé et pensé en une suite de fonction.

En espérant que cela vous a aidé, N.Martignole

Mai 2017

name := "Lunatech Scala tutorial"
version := "1.0"
scalaVersion := "2.11.8"
libraryDependencies += "org.scalactic" %% "scalactic" % "3.0.1"
libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.1" % "test"
parallelExecution in Test := false
logBuffered in Test := false
fork in Test := true
package com.lunatech.tutorial
import org.scalatest.Matchers._
import org.scalatest.WordSpec
/**
* In this test we want to implemented a MinecraftToolsGenerator.
*
* 1) Define ToolType as an Enumeration in Scala with the following values = AXE, SWORD, SHOVEL
* 2) Define Material as an Enumeratin with DIAMOND, IRON and WOOD
* 3) Create a Weapon type, that has a toolType and a material
* for instance a Wooden Sword is Weapon(ToolType.SWORD, Material.WOOD)
*
* Create also an Ingredient case class that has a quantity and a material
*
* Now create the MinecraftToolsGenerator singleton and add the 2 following types
* - type WeaponAndIngredients = (Weapon, Option[Ingredient])
* - type Recipe = Ingredient => WeaponAndIngredients
*
* 4) Define a "coreRecipe" function with the following signature
* private def coreRecipe(ingredient: Ingredient, t: ToolType, requiredQuantity: Int): (Weapon, Option[Ingredient]) = {
*
* A Sword requires 2 Ingredients to be build
* An Axe requires 3 Ingredients
* A Shovel requires only 1 ingredient
*
* 5) Define a swordRecipe function of type Recipe that will use coreRecipe amd set the quantity
* required to build a sword to 2. Read carefully the Recipe type but use the coreRecipe function
*
* def swordRecipe: Recipe = ....
*
* 6) Define the axeRecipe function of type Recipe that will use coreRecipe
*
* 7) Define the shovelRecipe function of type Recipe that will use coreRecipe
*
* 8) We should now be able to implement the buildAxe function
*
* Before that, create the private craft function as follow =>
* def craft(recipe: Recipe, ingredient: Ingredient): Either[String, WeaponAndIngredients] = {
* recipe(ingredient) match {
* case s if s._2.isDefined && s._2.get.quantity < 0 => Left("Not enough ingredients")
* case other => Right(other)
* }
* }
*
* 9) Now implement buildSword, buildAxe and buildShovel with the "craft" function and the various "recipe" you also implemented
*
* 10) TODO buildBestPackageFrom => Set[Weapon]
*
* Bonus : implement uncraft function => def uncraft: Weapon => Ingredient
*
*
*/
class TestMinecratToolsGenerator extends WordSpec {
"A Minecraft tools builder " when {
"the craft function is executed "
"it receives 2 Wood blocks" should {
"be able to build a wood sword" in {
val ingredients = Ingredient(2, Material.WOOD)
val expected = (Weapon(ToolType.SWORD, Material.WOOD), None)
MinecraftToolsGenerator.buildSword(ingredients) should be(Right(expected))
}
}
"it receives 4 Wood blocks" should {
"be able to build a wood sword and return 2 Ingredient of type Wood" in {
val ingredients = Ingredient(4, Material.WOOD)
val expected = Right(Weapon(ToolType.SWORD, Material.WOOD), Some(Ingredient(2, Material.WOOD)))
MinecraftToolsGenerator.buildSword(ingredients) should be(expected)
}
}
"it receives 1 Wood blocks" should {
"not be able to build a wood sword and return an error message" in {
val ingredients = Ingredient(1, Material.WOOD)
val expected = Left("Not enough ingredients to build a sword")
MinecraftToolsGenerator.buildSword(ingredients) should be(expected)
}
"not be able to build a wood axe and return an error message" in {
val ingredients = Ingredient(1, Material.WOOD)
val expected = Left("Not enough ingredients to build a axe")
MinecraftToolsGenerator.buildAxe(ingredients) should be(expected)
}
"be able to build a wood shovel" in {
val ingredients = Ingredient(1, Material.WOOD)
val expected = Right(Weapon(ToolType.SHOVEL, Material.WOOD), None)
MinecraftToolsGenerator.buildShovel(ingredients) should be(expected)
}
}
"the uncraft method is used" should {
"returns 2 Wood ingredients from a sword" in {
val sword = Weapon(ToolType.SWORD, Material.WOOD)
val expected = Ingredient(2, Material.WOOD)
MinecraftToolsGenerator.uncraft(sword) should be(expected)
}
"returns 2 IRON ingredients from a sword" in {
val sword = Weapon(ToolType.SWORD, Material.IRON)
val expected = Ingredient(2, Material.IRON)
MinecraftToolsGenerator.uncraft(sword) should be(expected)
}
}
"it has 0 ingredient" should {
"do nothing" in {
// GIVEN
val ingredients = Ingredient(0, Material.IRON)
MinecraftToolsGenerator.buildBestPackageFrom(ingredients) should be(Set.empty[Weapon])
}
}
"it has 1 Wood block" should {
"build 1 shove" in {
// GIVEN
val ingredients = Ingredient(1, Material.IRON)
val expected = Set(
Weapon(ToolType.SHOVEL, Material.IRON)
)
MinecraftToolsGenerator.buildBestPackageFrom(ingredients) should be(expected)
}
}
"it has 2 Wood block" should {
"build 1 Sword" in {
// GIVEN
val ingredients = Ingredient(2, Material.IRON)
val expected = Set(
Weapon(ToolType.SWORD, Material.IRON)
)
MinecraftToolsGenerator.buildBestPackageFrom(ingredients) should be(expected)
}
}
"it has 3 Wood block" should {
"build 1 Axe" in {
// GIVEN
val ingredients = Ingredient(3, Material.IRON)
val expected = Set(
Weapon(ToolType.AXE, Material.IRON)
)
MinecraftToolsGenerator.buildBestPackageFrom(ingredients) should be(expected)
}
}
"it has 4 Wood block" should {
"build 1 Axe, 1 Shovel " in {
// GIVEN
val ingredients = Ingredient(4, Material.IRON)
val expected = Set(
Weapon(ToolType.AXE, Material.IRON),
Weapon(ToolType.SHOVEL, Material.IRON)
)
MinecraftToolsGenerator.buildBestPackageFrom(ingredients) should be(expected)
}
}
"it has 5 Wood block" should {
"build 1 Axe and 1 sword" in {
// GIVEN
val ingredients = Ingredient(5, Material.IRON)
val expected = Set(Weapon(ToolType.SWORD, Material.IRON),
Weapon(ToolType.AXE, Material.IRON)
)
MinecraftToolsGenerator.buildBestPackageFrom(ingredients) should be(expected)
}
}
"it has 6 Wood blocks" should {
"build 1 sword and 1 axe and 1 shove" in {
// GIVEN
val ingredients = Ingredient(6, Material.IRON)
val expected = Set(Weapon(ToolType.SWORD, Material.IRON),
Weapon(ToolType.AXE, Material.IRON),
Weapon(ToolType.SHOVEL, Material.IRON)
)
MinecraftToolsGenerator.buildBestPackageFrom(ingredients) should be(expected)
}
}
}
}
package com.lunatech.tutorial
import com.lunatech.tutorial.Material.Material
import com.lunatech.tutorial.ToolType.ToolType
import scala.annotation.tailrec
object Material extends Enumeration {
type Material = Value
val DIAMOND, IRON, WOOD = Value
}
object ToolType extends Enumeration {
type ToolType = Value
val AXE, SWORD, SHOVEL = Value
}
case class Weapon(toolType: ToolType, material: Material) {
override def toString: String = toolType.toString.toLowerCase.capitalize + " in " + material.toString.toLowerCase
}
case class Ingredient(quantity: Int, material: Material)
/**
* @author Nicolas Martignole
*/
object MinecraftToolsGenerator {
type WeaponAndIngredients = (Weapon, Option[Ingredient])
type Recipe = Ingredient => WeaponAndIngredients
type RecipeBuilder = Ingredient => Either[String, WeaponAndIngredients]
def swordRecipe: Recipe = (ingredient: Ingredient) => ???
def axeRecipe: Recipe = (ingredient: Ingredient) => ???
def shovelRecipe: Recipe = (ingredient: Ingredient) => ???
val NoIngredient = Ingredient(0, Material.WOOD)
private def coreRecipe(ingredient: Ingredient, t: ToolType, requiredQuantity: Int): (Weapon, Option[Ingredient]) = ???
def buildSword: RecipeBuilder = ???
def buildAxe: RecipeBuilder = ???
def buildShovel: RecipeBuilder = ???
def uncraft: Weapon => Ingredient = ???
protected def craft(recipe: Recipe, ingredient: Ingredient, name: String): Either[String, WeaponAndIngredients] = ???
def buildBestPackageFrom(ingredients: Ingredient): Set[Weapon] = ???
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment