From adc2df902bfa9b9685a3b65eabaeffc32276874e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20=C3=81ngel=20M=C3=A9ndez=20Gort?= Date: Mon, 1 Apr 2024 12:17:42 +0200 Subject: [PATCH] BFS draft --- .editorconfig | 7 ++ .gitignore | 1 + bfs.ipynb | 274 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 282 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 bfs.ipynb diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..75e10ec --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +[*.fs] +indent_size = 2 +end_of_line = lf + +[*.fsx] +indent_size = 2 +end_of_line = lf \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eaa9c18 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +tests.fsx diff --git a/bfs.ipynb b/bfs.ipynb new file mode 100644 index 0000000..6db01c3 --- /dev/null +++ b/bfs.ipynb @@ -0,0 +1,274 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Revisiting BFS" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### What is BFS\n", + "- BFS is a way of creating a sequence with all elements of a tree or graph\n", + "- It consists in returning all elements with depth N, and then the elements with depth N+1, until elements are over\n", + "- The initial depth is 0\n", + "\n", + "##### The common solution\n", + "- The typical imperative implementation of BFS relies on a queue\n", + "- The current element is pulled out of the queue, and its children are pushed in\n", + "\n", + "##### Questioning the common solution\n", + "- The relation between the queue, the way it works, and the BFS definition is not clear\n", + "- Is it possible to rely on pure functional data structures instead of mutation?\n", + "\n", + "##### First discovery\n", + "- The queue in the imperative solution is divided in two parts: `| children with depth N | children with depth N+1|`\n", + "- Now is clear what we do is returning children in the same level, and meanwhile conveniently recording and postponing children in the next level\n", + "- We could have separate lists for children in different levels\n", + "- We could annotate each level with its corresponding number, so we can filter later which levels do we need\n", + "\n", + "##### First restriction\n", + "- BFS in a graph requires to mark which nodes we have visited, while in a tree we don't, \n", + "since in the latter case there's no possibility to create an infinite loop\n", + "- the function returning the children can take care independently of marking or not, depending on wether is a graph or a tree.\n", + "That way the BFS implementation can be delivered from that concern, and thus is cleaner and more general.\n", + "\n", + "##### Second restriction\n", + "- In functional programming instead of representing mutation in a block of code, we create a new instance of it\n", + "substituting the old value by the new one.\n", + "- Example:\n", + "\n", + "```csharp\n", + "using System;\n", + "\n", + "int[] MoveFromTo() {\n", + " var from = new List(new int[] { 1, 2, 3 });\n", + " var to = new List();\n", + " while (from.Count > 0) {\n", + " to.Add(from[0]);\n", + " from.RemoveAt(0);\n", + " }\n", + " return to.ToArray();\n", + "}\n", + "```\n", + "\n", + "```fsharp\n", + "let moveFromTo () =\n", + " let rec loop from to' =\n", + " match from with\n", + " | [] -> (from, to')\n", + " | x::xs -> \n", + " // here we replace to' by x::to' in a new instance of loop\n", + " loop xs (x::to')\n", + " \n", + " let from = [1; 2 ;3]\n", + " let to' = []\n", + " loop from to'\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### How the implementation works" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "- `GetChildren` is our generic way of getting children from whatever nested structure we are dealing with (trees, graphs)\n", + "- Along with the list of children it returns and instance of itself. This is needed because is the functional way of updating the collection where we store visited nodes.\n", + "- Our `List.fold` processes each element `x` in `level`. In each iteration of it needs an updated `GetChildren` instance, which stores the visited nodes. You can see in the function passed as parameter how `xs` is replaced by `xs @ ys` and `gc` for `newGc`, for the next iteration.\n", + "- Once the fold finishes we have in `nextLevel` all the nodes of level `n+1`. Since `nextLevel` is the replacement for `level` and our `bfs` function operates on particular level instances, then is time to call `bfs` with `nextLevel`. In case `nextLevel` is empty, we return the empty sequence and stop the recursive calls." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "dotnet_interactive": { + "language": "fsharp" + }, + "polyglot_notebook": { + "kernelName": "fsharp" + }, + "vscode": { + "languageId": "polyglot-notebook" + } + }, + "outputs": [], + "source": [ + "type GetChildren<'a> = { get: 'a -> (GetChildren<'a> * 'a list) }\n", + "\n", + "let rec bfs (gc: GetChildren<'a>, n: int, level: 'a list) =\n", + " seq {\n", + " yield (n, level)\n", + "\n", + " let nextLevel =\n", + " level\n", + " |> List.fold\n", + " (fun (gc, xs) x ->\n", + " let newGc, ys = gc.get x\n", + " (newGc, xs @ ys)\n", + " )\n", + "\n", + " (gc, []) // initial state\n", + " \n", + " yield! \n", + " match nextLevel with\n", + " | _, [] -> Seq.empty\n", + " | (chl, nl) -> bfs (chl, n+1, nl)\n", + " }\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### Testing the implementation with a graph" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "dotnet_interactive": { + "language": "fsharp" + }, + "polyglot_notebook": { + "kernelName": "fsharp" + }, + "vscode": { + "languageId": "polyglot-notebook" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(0, [1])\n", + "(1, [2; 3; 4])\n", + "(2, [5; 6; 7; 8; 9; 10])\n" + ] + } + ], + "source": [ + "type Graph<'a when 'a: comparison> = Map<'a, List<'a>>\n", + "\n", + "let graphChildren (g: Graph<'a>) =\n", + " let rec children visited =\n", + " { get =\n", + " fun x ->\n", + " if Set.contains x visited then\n", + " // x is already visited, no need to mark it as visited\n", + " (children visited, [])\n", + " else\n", + " let newChildren = Set.add x visited |> children\n", + "\n", + " match Map.tryFind x g with\n", + " | Some xs -> (newChildren, xs)\n", + " | None -> (newChildren, []) }\n", + "\n", + " children Set.empty\n", + "\n", + "let g = Map [ 1, [ 2; 3; 4 ]; 2, [ 5; 6 ]; 3, [ 7; 8 ]; 4, [ 9; 10 ] ]\n", + "\n", + "bfs (graphChildren g, 0, [ 1 ]) |> Seq.iter (printfn \"%A\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "##### Testing the implementation with a tree" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In a tree by visiting a node there's no way of going back to it, since there are no cycles. Keeping that in mind, \n", + "we can rely on a sequence of children to be consumed by `bfs`. As long as we put children in the order `bfs` expects them, the implementation will be correct.\n", + "\n", + "Example:\n", + "We know if `bfs` gets the children of node `X`, then after that it will ask for children of node `X+1`, where `X` and `X+1` are consecutive nodes in the same level." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "dotnet_interactive": { + "language": "fsharp" + }, + "polyglot_notebook": { + "kernelName": "fsharp" + }, + "vscode": { + "languageId": "polyglot-notebook" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(0, [1])\n", + "(1, [2; 3; 4])\n", + "(2, [5; 6; 7; 8; 9; 10])\n" + ] + } + ], + "source": [ + "type Tree<'a> = Node of 'a * Tree<'a> list\n", + "\n", + "let rec childrenSeq (Node(_, xs)) =\n", + " seq {\n", + " match xs with\n", + " | [] -> ()\n", + " | _ ->\n", + " let nodeValues = xs |> List.map (fun (Node(x, _)) -> x)\n", + " yield nodeValues\n", + "\n", + " for x in xs do\n", + " yield! childrenSeq x\n", + " }\n", + "\n", + "let treeChildren (t: Tree<'a>) =\n", + " let chs = childrenSeq t |> Seq.toList\n", + "\n", + " let rec loop (chs: ('a list) list) =\n", + " { get =\n", + " fun _ ->\n", + " match chs with\n", + " | [] -> (loop [], [])\n", + " | head::tail -> (loop tail, head) }\n", + "\n", + " loop chs\n", + "\n", + "let t =\n", + " Node(\n", + " 1,\n", + " [ Node(2, [ Node(5, []); Node(6, []) ])\n", + " Node(3, [ Node(7, []); Node(8, []) ])\n", + " Node(4, [ Node(9, []); Node(10, []) ]) ]\n", + " )\n", + "\n", + "bfs (treeChildren t, 0, [ 1 ]) |> Seq.iter (printfn \"%A\")" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +}