How to get outer scope variable in nested for-loop?

Hi, Dear all…

i wanna generate list from nested For-Loop
the inside logic depend on out scope variable and total count

here is my testing code…

def generateResults do

	items = [1, 2, 3, 4, 5]
	defines =
	[
		%{ :Id => 0, :Count => %{ :Min => 1, :Max => 2 } },
		%{ :Id => 1, :Count => %{ :Min => 1, :Max => 5 } }
	]

	totalGenerateDefines =
	for define <- defines, into: [] do

		if :rand.unifrom <= 0.5 do
			[]
		else

			id       = define[:Id]
			maxCount = define[:Count][:Max]

			childs =
			for idx <- 1..maxCount, into: [] do

				map = %{ :id => id }
				needAddItem = false
				if length( items ) <= length( totalGenerateDefines ) do
					needAddItem = true
				else
					if :rand.uniform <= 0.5, do: needAddItem = true
				end

				if needAddItem do
					{ result, items } = List.pop_at( items, 0 )
					map = Map.put( map, :itemId, result )
				end

				map
			end
		end
	end
end

and expect result is…


test "ForLoop Nested generate list" do
	
	
	result = generateResults()
	
	#Expect Result Like..
	results =
	[
		%{ :id => 0 },
		%{ :id => 0, :itemId => 1 },
		%{ :id => 0 },
		%{ :id => 0 },
		%{ :id => 1, :itemId => 2 },
		%{ :id => 1, :itemId => 3 },
		%{ :id => 1 },
		%{ :id => 1, :itemId => 4 },
		%{ :id => 1, :itemId => 5 }
	]
	
end

but i have no idea how to fix it, anyone can tech me, please?

thank you :slight_smile:

So, it’s quite possible for someone to just give you an answer here, but I think if someone does so they are doing you a disservice. The code you have here indicates that you haven’t quite internalized that Elixir is a language with immutable data.

Start with something simple. How would you count a simple list?

Given items = [1, 2, 3, 4, 5] how do you count the number of things inside items without using the built in length? Your choices are manual recursion, or Enum.reduce. For the purposes of figuring out your more nested list situation, Enum.reduce/3 is the function you’ll want to look at.

8 Likes

yes, i’m beginner of elixir :slight_smile:

currently i using recursion to resolve this,

here is new version…

def generateResults do

	items = [1, 2, 3, 4, 5]

	defines =
	[
		%{ :Id => 0, :Count => %{ :Min => 3, :Max => 6 } },
		%{ :Id => 1, :Count => %{ :Min => 2, :Max => 6 } }
	]

	results = genResultsBy( defines, 0, [], items, 0 )

	IO.puts "Results: #{ inspect results }"
end

level one rescursion part…


def genResultsBy( defines, idx, array, items, totalCount ) do

	lengthDefines = length( defines )
	if lengthDefines > idx do

		define = Enum.at( defines, idx )

		lists =
		if false do

			[]

		else

			id       = define[:Id]
			maxCount = define[:Count][:Max]
			count    = :rand.uniform( maxCount )
			{ records, items, totalCount } = genChildsBy( id, [], items, count, totalCount )
			records
		end

		genResultsBy( defines, idx + 1, array ++ lists, items, totalCount )

	else

		array

	end
end

and level two…

def genChildsBy( id, records, items, countNeed, totalCount ) do

	if countNeed <= 0 do

		{ records, items, totalCount }
	else

		totalCount = totalCount + 1
		countNeed = countNeed - 1
		defaultMap = %{ :Id => id }

		needAddItem = false
		if length( items ) <= totalCount do
			needAddItem = true
		else
			if :rand.uniform <= 0.8, do: needAddItem = true
		end

		if needAddItem && length( items ) > 0 do

			{ result, items } = List.pop_at( items, 0 )
			defaultMap = Map.put( defaultMap, :itemId, result )

		end

		records = records ++ [defaultMap]

		genChildsBy( id, records, items, countNeed, totalCount )

	end

end

can you give me more advise ? thank you :smiley:

Can you explain more what the algorithm is intended to do? The code is quite hard to reason about (for me).

I think the idea is to generate a list of maps where each map has an :id and for each :id there are 0…n maps where n is :rand.uniform(max), For each map there may be an :item_id of some value that comes from items (but Im not sure how you decide which item.

in Elixir if you’re using Enum.at/2 and indexes into lists its a pretty good chance you’re not in the Elixir or functional groove.

2 Likes

Hi, thanks for reply,

this code is for testing version,
so i modified code for easy read

yes,
the final target is generate a records list from config,
and each record have chance get item from list items
list items like a item pool, when take one, remove one ( so i using pop_at 0 )

my first question is i can’t access items list from nested for-loop :slight_smile:

First thing, try to stop thinking about for-loop. There isn’t a thing like a loop in Eixir. A loop requires to mutate the state for each iteration, Elixir is immutable.

Second thing, dont try to reproduce solution from other languages. Start with the problem, or “what is the final intent of your code”, and from that find the resources the language provides you to achieve that.

1 Like

Just one small example of how sometimes the thinking in Elixir is just a little “different”:

{ result, items } = List.pop_at( items, 0 )
map = Map.put( map, :itemId, result )

The head of a list is typically obtained through a pattern match on the list

[item|other_items] = items
new_map = Map.put map, :item_id, item

The pattern match can even occur right on top of an argument in a function parameter like in Kilo.add_item_r/2 below:

# file: kilo.exs
defmodule Kilo do

  def put_item(record, item),
    do: Map.put record, :item, item

                                                         # -- function clause with pattern matching
  def add_item_r(record, {[item|other_items], result}),  # the tuple, the list and it's head and tail
    do: {other_items, [(put_item record, item)|result]}  # same list syntax is used to build a new list


  def add_item_m({item, record}),                        # pattern matching a tuple
    do: put_item record, item

                                                         # -- multi-clause function
  def add_item_c({:none, record}),                       # each clause processing a separate case
    do: record                                           # identified by pattern matching
  def add_item_c({item, record}),                        # pattern matching is a conditional construct
      do: put_item record, item

end

ids = Enum.to_list 0..9                     # Using a range to create a list
IO.puts "ids #{inspect ids}"

items = for id <- ids, do: 10 - id          # for comprehension
IO.puts "items #{inspect items}"

items_again = Enum.map ids, &(10 - &1)      # same thing - this time with Enum.map
IO.puts "item_again #{inspect items_again}" # with a captured partial function application

recs = Enum.map ids, &(Map.new [{:id, &1}]) # creating Map with a key-value tuple list
IO.puts "recs #{inspect recs}"

{_,recsr} = Enum.reduce recs, {items,[]}, &Kilo.add_item_r/2 # Adding items using reduce
IO.puts "recsr #{inspect recsr}"                             # with a captured function
                                                             # note the order of the result
IO.puts "Enum.reverse recsr #{inspect (Enum.reverse recsr)}"

recsz =
  items                                                      # again same thing
  |> Enum.zip(recs)                                          # this time with
  |> Enum.map(&Kilo.add_item_m/1)                            # zip-mapping with the pipe operator
IO.puts "recsz #{inspect recsz}"                             # with a captured function

scatter_items = [:none,1,:none,:none,2,:none,:none,:none,3,:none]
recsc =
  scatter_items                                              # conditional adding
  |> Enum.zip(recs)                                          # using zip-mapping and
  |> Enum.map(&Kilo.add_item_c/1)                            # the multi-clause function
IO.puts "recsc #{inspect recsc}"                             # `Kilo.add_item_c/1`

.

$elixir kilo.exs
...
ids [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
items [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
item_again [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
recs [%{id: 0}, %{id: 1}, %{id: 2}, %{id: 3}, %{id: 4}, %{id: 5}, %{id: 6}, %{id: 7}, %{id: 8}, %{id: 9}]
recsr [%{id: 9, item: 1}, %{id: 8, item: 2}, %{id: 7, item: 3}, %{id: 6, item: 4}, %{id: 5, item: 5}, %{id: 4, item: 6}, %{id: 3, item: 7}, %{id: 2, item: 8}, %{id: 1, item: 9}, %{id: 0, item: 10}]
Enum.reverse recsr [%{id: 0, item: 10}, %{id: 1, item: 9}, %{id: 2, item: 8}, %{id: 3, item: 7}, %{id: 4, item: 6}, %{id: 5, item: 5}, %{id: 6, item: 4}, %{id: 7, item: 3}, %{id: 8, item: 2}, %{id: 9, item: 1}]
recsz [%{id: 0, item: 10}, %{id: 1, item: 9}, %{id: 2, item: 8}, %{id: 3, item: 7}, %{id: 4, item: 6}, %{id: 5, item: 5}, %{id: 6, item: 4}, %{id: 7, item: 3}, %{id: 8, item: 2}, %{id: 9, item: 1}]
recsc [%{id: 0}, %{id: 1, item: 1}, %{id: 2}, %{id: 3}, %{id: 4, item: 2}, %{id: 5}, %{id: 6}, %{id: 7}, %{id: 8, item: 3}, %{id: 9}]
$

I don’t think this is exactly your algorithm, but its close and its more idiomatic Elixir (likely far from perfect however):

defmodule GenResult do
  def generate do
  	items = [1, 2, 3, 4, 5]

  	definitions = [
  		%{ id: 0, count: %{ min: 1, max: 2 } },
  		%{ id: 1, count: %{ min: 1, max: 5 } }
  	]

    {records, _items} = Enum.reduce definitions, {[], items}, fn definition, {results, items} ->
      {records, items} = define_items(definition, items, :rand.uniform())
      {records ++ results, items}
    end

    records
  end

  # Empty list result
  def define_items(_definition, items, probability) when probability < 0.5 do
    {[], items}
  end

  # No more items to be consumed
  def define_items(_definition, [], _probability) do
    {[], []}
  end

  def define_items(%{id: id, count: %{max: max}}, items, _probability) do
    gen_for_define(id, :rand.uniform(max), :rand.uniform(), items, 0, [])
  end

  # No more items left
  def gen_for_define(_id, _max, _add_item, [] = items, _count, records) do
    {records, items}
  end

  # Maximum number of records generated for this definition
  def gen_for_define(_id, max, _add_item, items, count, records) when count > max do
    {records, items}
  end

  # Generate a record with a child
  def gen_for_define(id, max, add_item, [item | rest], count, records) when add_item < 0.8 do
    new_records = [%{id: id, item_id: item} | records]
    gen_for_define(id, max, :rand.uniform(), rest, count + 1, new_records)
  end

  # Generate a bare record
  def gen_for_define(id, max, _add_item, items, count, records) do
    new_records = [%{id: id} | records]
    gen_for_define(id, max, :rand.uniform(), items, count + 1, new_records)
  end
end

In the terminal:

iex(27)> GenResult.generate
[%{id: 1, item_id: 5}, %{id: 1}, %{id: 0, item_id: 3}, %{id: 0}, %{id: 0}]
iex(28)> GenResult.generate
[]
iex(29)> GenResult.generate
[%{id: 0, item_id: 3}, %{id: 0, item_id: 2}, %{id: 0}]
iex(30)> GenResult.generate
[%{id: 1, item_id: 5}, %{id: 1, item_id: 4}, %{id: 1, item_id: 3},
 %{id: 1, item_id: 2}, %{id: 1}]
3 Likes

okay, thanks for your suggestion,
i will try to fighting to this goal :smiley:

forget other language pattern, think like Elixir.

OH My God,
this is so Perfect !!

I never thought Elixir could write like this,

Thank you very much for your guidance,
really let me get a valuable lesson,

I will continue to work hard, hope the future can be the same as you write so pretty the code

thank you again :heart_eyes:

3 Likes