Functorio

Functorio lets you build your Factorio factories in the Lean programming language; giving you conveniences like type safety, functions, recursion, version history, libraries, etc.

For example, here's a simple factory that generates 150 red science per minute.

def redScience := bus do
  let ironOre <- input .ironOre 300
  let copperOre <- input .copperOre 150

  let iron : Iron 300 <- makeIron ironOre
  let copper : Copper 150 <- makeCopper copperOre
  let gear : Gear 150 <- makeGear iron
  let science : RedScience 150 <- makeRedScience copper gear

Functorio let's you export your factories as blueprints, ready to be imported into Factorio:

In addition to making it easy to write factories, Functiorio also makes it safe! Every resource that is passed around (like iron, gear, science, ...) has a type indicating the kind of item and the number of items provided per minute. In addition, every factory (like makeIron, makeGear, makeRedScience, ...) indicates how many resources it takes and returns. Thus, when your factory type checks, it often just works. And here are the stats to prove it:

To get started download the github repo and open it in VSCode. The github repo contains a devcontainer so VSCode will automatically install all necessary dependencies and you can just run:

./red-science-150.sh > blueprint.txt

This will print a blueprint that you can import directly into Factorio.

This library is still in early alpha. Let's make it better together! Join the discord, send me pull requests, file github issues, email me, etc. Anything you think is interesting is probably also interesting to me.

I'm not the first person to have looked into the connection between Factorio and programming languages. Bartosz Milewski gave a talk and wrote a great blog post about the connection; and also came up with the name Functorio in his post.

The following sections describe Functorio's features in more detail. At the end of this post you'll see how to build a factory that generates 150 of all the Nauvis sciences (red, green, blue, yellow, purple, black) per minute.

Buses

A bus lets you connect multiple factories in a conventient a safe way. Here is an example bus that makes gears from iron ore. You just specify how the resources should flow, and the bus function creates the required bus entities (belts, pipes, etc) to realize that intention. The way factories get connected resembles the main bus pattern, hence the name.

Here's a simple gear bus example:

def gearFactory := bus do
  let ironOre : IronOre 300 <- input .ironOre 300
  let iron : Iron 300 <- makeIron ironOre
  let gear <- makeGear iron

The variables on a bus (e.g. ironOre, iron, gear) represent the lanes of the bus, and have the type BusLane ingredient throughput. For example ironOre has the type BusLane .ironOre 100 which means this lane carries 100 iron ore per minute. The library also provides shorter name for the types, e.g. IronOre 100 is a synonym for BusLane .ironOre 100. A bus lane can be consumed by passing it into a factory, e.g. makeIron ironOre. New lanes can be returned by factories, for example the makeGear factory creates the new gear lane.

The input command creates a new bus lane that is expected to be filled outside the factory. This is useful to provide the raw ingredients for your factory. All inputs must be declared at the beginning of the bus, before any factories are used.

Bus lanes must only ever be consumed once. It is invalid to pass the same lane into two factories. If this is desired, the lane must first be split into two lanes, using the split (left:=x) (right:=y) function. Here is an example:

def splitIron := bus do
  let iron : Iron 450 <- input .ironPlate 450
  let (iron0, iron1) <- split (left:=300) (right:=150) iron
  let gear <- makeGear iron0
  let belt <- makeBelt iron1 gear

Similarly, lanes can be merged with the merge function. Here is an example:

def mergeSteel := bus do
  let iron0 <- input .ironPlate 2700
  let iron1 <- input .ironPlate 2700
  let steel0 <- makeSteel iron0
  let steel1 <- makeSteel iron1
  let steel <- merge steel0 steel1

The bus also supports liquids. Those are used just like solid ingredients, but the bus will generate pipes instead of belts to connect them. e.g.

def acidFactory := bus do
  let iron <- input .ironPlate 60
  let sulfur <- input .sulfur 300
  let water <- input .water 6000
  let acid <- makeAcid water iron sulfur

Bus Taps

We've talked about passing resources around the bus, but we still haven't talked about how makeBelt etc is actually implemented. To connect a factory to the bus, you need to create a busTap, which takes a list of input belt lanes from the bus, connects to the given factory and returns the output of the factory. For example, makeBelt can be implemented as follows

def makeBelt iron gear :=
   busTap [iron, gear] factory

Factories

A Factory is a collection of entities (assemblers, belts, inserters, etc) placed within a rectangle, and has interfaces on all sides of that rectangle (north, east, south and west). For the below example, the north and south interface are the same: two belts, one iron belt facing north and one gear belt facing south. The east and west interfaces are empty.

The type of a factory is Factory n e s w where the letters stand for the north, east, south, and west interfaces. Each interface specifies the resource type and travel direction of that resource. Thus the above factory's type is:

gearFactory : Factory 
  [(.iron, .N), (.ironGearWheel, .S)]  -- north interfaces
  []                                   -- east interfaces
  [(.iron, .N), (.ironGearWheel, .S)]  -- south interfaces
  []                                   -- west interfaces

The Factory type is designed so that you should only have to worry about a factory's interfaces, but not its size, entities, or the position of the interfaces. The interfaces are reflected in the Factory's type, so when your factory type checks, it should ideally just work.

Columns / Rows

Two factories with matching north/south interfaces can be combined with the column command. Here's an example:

column gearFactory gearFactory

There is also a columnList command, which connects all the factories from the given list. To create a column of 10 gear factories, you would write:

columnList (List.replicate 10 gearFactory)

Similarly, there is also a row/rowList command, which combines factories with matching east/west interfaces.

Expanding

Sometimes the factories that you want to combine don't have the same dimensions. Expanding a factory increases the factory's width/height without changing its interfaces. For example, here we're expanding a factory's south by 5 tiles:

expand .S 5 inserterFactory

Expansions are automatically generated when you combine factories with column/row, so you generally don't have to worry about the dimensions of factories.

Adapters

Sometimes the factories that you want to combine have the sames interfaces, but the belts/pipes are in different positions. That's where adapters come in. For example, here we're trying to connect some inserter assemblers to a bus.

Adapters are automatically generated when you combine factories with column/row, so you generally don't have to worry about the position of interfaces (but the order of interfaces matters).

Caps

Sometimes you want to remove an interface from a factory. The capN, capE, capS, capW commands do just that.

capS inserterFactory

Assembly Lines

Functorio comes with a builtin set of factories to assemble all the recipes required for red, green, blue, purple, yellow, and black science (contributions for more recipes are very welcome!). These factories are designed so that they can be replicated many times into an assembly line.

To get just a single assembler for a certain recipe, you can use the station function (because it's one station of a larger assembly line):

station .ironGearWheel

If you want to have multiple stations connected, you can call:

assemblyLine .ironGearWheel 3

Calling assemblyLine is similar to just replicating the station multiple times, but the assemblyLine command will also automatically insert output load balancers, roboports, passive provider chests, and big electric poles if desired.

To get a factory that can be attached to a bus, call:

busAssemblyLine .ironGearWheel 3

Checkout the file NauvisScience150.lean for a full factory that uses the assembly line library for all its needs. You can generate a blueprint as follows:

./nauvis-150.sh > blueprint.txt

Which looks like this:

And as promised, produces 150 of all the Nauvis sciences per minutes.

Configuration

You can configure how Functorio generates certain aspects of your factory, by providing a Config instance.

instance : Config where
  generateRoboports := true   -- assembly lines add roboports
  generateBigPoles := true    -- assembly lines add big poles
  providerChestCapacity := 5  -- assembly lines add provider chests for outputs
  adapterMinHeight := 3       -- minimum height of adapters

Debugging

Sometimes you can't convince Lean that two factories have the same interface, even though you know that they do. You can use the factory.unsafeCastFactory to change the interface of your factory to whatever you want.

Lean isn't all that great a printing types. For example, Lean will print the type of busAssemblyLine .processingUnit 14 as BusAssemblyLineType (getRecipe .processingUnit) 14. This is really quite useless. If you want to know what that type expands to, you can run the following code, and see the simplified type in Lean's InfoView.

def x : BusAssemblyLineType RecipeName.processingUnit 14 := by
  simp!

Which is:

BusLane Ingredient.sulfuricAcid (525, 1) →
BusLane Ingredient.electronicCircuit (2100, 1) →
BusLane Ingredient.advancedCircuit (210, 1) →
Bus (BusLane Ingredient.processingUnit (105, 1))

Future Work

This library is still in early alpha. Let's make it better together! Expect bugs! For any problems, please reach out on discord, file a ticket on github or contact me via email. If you have a Lean problem, there are many really helpful people in this chat.

There are still a lot of things that need modeling: e.g. other kinds of belts and inserters, modules, beaconized factories, quality, rails, etc. I'm happy to take contributions!